diff options
911 files changed, 49864 insertions, 8708 deletions
diff --git a/.gitattributes b/.gitattributes index 5af3e121a8..30d1acb497 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,6 +1,6 @@ # Properly detect languages on Github -*.h linguist-language=cpp -*.inc linguist-language=cpp +*.h linguist-language=C++ +*.inc linguist-language=C++ thirdparty/* linguist-vendored # Normalize EOL for all files that Git considers text files diff --git a/.github/workflows/windows_builds.yml b/.github/workflows/windows_builds.yml index 90629204e6..0c21576517 100644 --- a/.github/workflows/windows_builds.yml +++ b/.github/workflows/windows_builds.yml @@ -30,6 +30,14 @@ jobs: # Skip debug symbols, they're way too big with MSVC. sconsflags: debug_symbols=no vsproj=yes vsproj_gen_only=no windows_subsystem=console bin: "./bin/godot.windows.editor.x86_64.exe" + artifact: true + + - name: Editor w/ clang-cl (target=editor, tests=yes, use_llvm=yes) + cache-name: windows-editor-clang + target: editor + tests: true + sconsflags: debug_symbols=no windows_subsystem=console use_llvm=yes + bin: ./bin/godot.windows.editor.x86_64.llvm.exe - name: Template (target=template_release) cache-name: windows-template @@ -37,6 +45,7 @@ jobs: tests: true sconsflags: debug_symbols=no tests=yes bin: "./bin/godot.windows.template_release.x86_64.console.exe" + artifact: true steps: - uses: actions/checkout@v4 @@ -84,10 +93,12 @@ jobs: continue-on-error: true - name: Prepare artifact + if: ${{ matrix.artifact }} run: | Remove-Item bin/* -Include *.exp,*.lib,*.pdb -Force - name: Upload artifact + if: ${{ matrix.artifact }} uses: ./.github/actions/upload-artifact with: name: ${{ matrix.cache-name }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 46f29d0d5f..6cc6a211f1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,6 +17,7 @@ repos: exclude: | (?x)^( tests/python_build/.*| + platform/android/java/editor/src/main/java/com/android/.*| platform/android/java/lib/src/com/.* ) @@ -30,6 +31,7 @@ repos: exclude: | (?x)^( tests/python_build/.*| + platform/android/java/editor/src/main/java/com/android/.*| platform/android/java/lib/src/com/.* ) additional_dependencies: [clang-tidy==18.1.1] @@ -54,6 +56,11 @@ repos: rev: v2.3.0 hooks: - id: codespell + exclude: | + (?x)^( + platform/android/java/editor/src/main/java/com/android/.*| + platform/android/java/lib/src/com/.* + ) additional_dependencies: [tomli] ### Requires Docker; look into alternative implementation. @@ -135,6 +142,7 @@ repos: (?x)^( core/math/bvh_.*\.inc$| platform/(?!android|ios|linuxbsd|macos|web|windows)\w+/.*| + platform/android/java/editor/src/main/java/com/android/.*| platform/android/java/lib/src/com/.*| platform/android/java/lib/src/org/godotengine/godot/gl/GLSurfaceView\.java$| platform/android/java/lib/src/org/godotengine/godot/gl/EGLLogWrapper\.java$| @@ -162,6 +170,7 @@ repos: modules/gdscript/tests/scripts/parser/features/mixed_indentation_on_blank_lines\.gd$| modules/gdscript/tests/scripts/parser/warnings/empty_file_newline_comment\.notest\.gd$| modules/gdscript/tests/scripts/parser/warnings/empty_file_newline\.notest\.gd$| + platform/android/java/editor/src/main/java/com/android/.*| platform/android/java/lib/src/com/google/.* ) diff --git a/COPYRIGHT.txt b/COPYRIGHT.txt index a9d6cd7d32..5b6dcbb567 100644 --- a/COPYRIGHT.txt +++ b/COPYRIGHT.txt @@ -70,7 +70,8 @@ Copyright: 2020, Manuel Prandini 2007-2014, Juan Linietsky, Ariel Manzur License: Expat -Files: ./platform/android/java/lib/aidl/com/android/* +Files: ./platform/android/java/editor/src/main/java/com/android/* + ./platform/android/java/lib/aidl/com/android/* ./platform/android/java/lib/res/layout/status_bar_ongoing_event_progress_bar.xml ./platform/android/java/lib/src/com/google/android/* ./platform/android/java/lib/src/org/godotengine/godot/input/InputManagerCompat.java diff --git a/SConstruct b/SConstruct index 94574aacb2..0297cd6e61 100644 --- a/SConstruct +++ b/SConstruct @@ -234,7 +234,14 @@ opts.Add(BoolVariable("dev_mode", "Alias for dev options: verbose=yes warnings=e opts.Add(BoolVariable("tests", "Build the unit tests", False)) opts.Add(BoolVariable("fast_unsafe", "Enable unsafe options for faster rebuilds", False)) opts.Add(BoolVariable("ninja", "Use the ninja backend for faster rebuilds", False)) +opts.Add(BoolVariable("ninja_auto_run", "Run ninja automatically after generating the ninja file", True)) +opts.Add("ninja_file", "Path to the generated ninja file", "build.ninja") opts.Add(BoolVariable("compiledb", "Generate compilation DB (`compile_commands.json`) for external tools", False)) +opts.Add( + "num_jobs", + "Use up to N jobs when compiling (equivalent to `-j N`). Defaults to max jobs - 1. Ignored if -j is used.", + "", +) opts.Add(BoolVariable("verbose", "Enable verbose output for the compilation", False)) opts.Add(BoolVariable("progress", "Show a progress indicator during compilation", True)) opts.Add(EnumVariable("warnings", "Level of compilation warnings", "all", ("extra", "all", "moderate", "no"))) @@ -538,16 +545,22 @@ initial_num_jobs = env.GetOption("num_jobs") altered_num_jobs = initial_num_jobs + 1 env.SetOption("num_jobs", altered_num_jobs) if env.GetOption("num_jobs") == altered_num_jobs: - cpu_count = os.cpu_count() - if cpu_count is None: - print_warning("Couldn't auto-detect CPU count to configure build parallelism. Specify it with the -j argument.") + num_jobs = env.get("num_jobs", "") + if str(num_jobs).isdigit() and int(num_jobs) > 0: + env.SetOption("num_jobs", num_jobs) else: - safer_cpu_count = cpu_count if cpu_count <= 4 else cpu_count - 1 - print( - "Auto-detected %d CPU cores available for build parallelism. Using %d cores by default. You can override it with the -j argument." - % (cpu_count, safer_cpu_count) - ) - env.SetOption("num_jobs", safer_cpu_count) + cpu_count = os.cpu_count() + if cpu_count is None: + print_warning( + "Couldn't auto-detect CPU count to configure build parallelism. Specify it with the `-j` or `num_jobs` arguments." + ) + else: + safer_cpu_count = cpu_count if cpu_count <= 4 else cpu_count - 1 + print( + "Auto-detected %d CPU cores available for build parallelism. Using %d cores by default. You can override it with the `-j` or `num_jobs` arguments." + % (cpu_count, safer_cpu_count) + ) + env.SetOption("num_jobs", safer_cpu_count) env.extra_suffix = "" @@ -790,7 +803,7 @@ elif env.msvc: env.Append(CXXFLAGS=["/EHsc"]) # Configure compiler warnings -if env.msvc: # MSVC +if env.msvc and not methods.using_clang(env): # MSVC if env["warnings"] == "no": env.Append(CCFLAGS=["/w"]) else: @@ -840,8 +853,11 @@ else: # GCC, Clang # for putting them in `Set` or `Map`. We don't mind about unreliable ordering. common_warnings += ["-Wno-ordered-compare-function-pointers"] + # clang-cl will interpret `-Wall` as `-Weverything`, workaround with compatibility cast + W_ALL = "-Wall" if not env.msvc else "-W3" + if env["warnings"] == "extra": - env.Append(CCFLAGS=["-Wall", "-Wextra", "-Wwrite-strings", "-Wno-unused-parameter"] + common_warnings) + env.Append(CCFLAGS=[W_ALL, "-Wextra", "-Wwrite-strings", "-Wno-unused-parameter"] + common_warnings) env.Append(CXXFLAGS=["-Wctor-dtor-privacy", "-Wnon-virtual-dtor"]) if methods.using_gcc(env): env.Append( @@ -863,9 +879,9 @@ else: # GCC, Clang elif methods.using_clang(env) or methods.using_emcc(env): env.Append(CCFLAGS=["-Wimplicit-fallthrough"]) elif env["warnings"] == "all": - env.Append(CCFLAGS=["-Wall"] + common_warnings) + env.Append(CCFLAGS=[W_ALL] + common_warnings) elif env["warnings"] == "moderate": - env.Append(CCFLAGS=["-Wall", "-Wno-unused"] + common_warnings) + env.Append(CCFLAGS=[W_ALL, "-Wno-unused"] + common_warnings) else: # 'no' env.Append(CCFLAGS=["-w"]) @@ -1019,7 +1035,9 @@ if env["vsproj"]: if env["compiledb"]: if env.scons_version < (4, 0, 0): # Generating the compilation DB (`compile_commands.json`) requires SCons 4.0.0 or later. - print_error("The `compiledb=yes` option requires SCons 4.0 or later, but your version is %s." % scons_raw_version) + print_error( + "The `compiledb=yes` option requires SCons 4.0 or later, but your version is %s." % scons_raw_version + ) Exit(255) env.Tool("compilation_db") @@ -1031,13 +1049,10 @@ if env["ninja"]: Exit(255) SetOption("experimental", "ninja") + env["NINJA_FILE_NAME"] = env["ninja_file"] + env["NINJA_DISABLE_AUTO_RUN"] = not env["ninja_auto_run"] env.Tool("ninja") - # By setting this we allow the user to run ninja by themselves with all - # the flags they need, as apparently automatically running from scons - # is way slower. - SetOption("disable_execute_ninja", True) - # Threads if env["threads"]: env.Append(CPPDEFINES=["THREADS_ENABLED"]) diff --git a/core/SCsub b/core/SCsub index 1bd4eae16c..c8267ae960 100644 --- a/core/SCsub +++ b/core/SCsub @@ -140,7 +140,7 @@ if env["builtin_zstd"]: "decompress/zstd_decompress_block.c", "decompress/zstd_decompress.c", ] - if env["platform"] in ["android", "ios", "linuxbsd", "macos"]: + if env["platform"] in ["android", "ios", "linuxbsd", "macos"] and env["arch"] == "x86_64": # Match platforms with ZSTD_ASM_SUPPORTED in common/portability_macros.h thirdparty_zstd_sources.append("decompress/huf_decompress_amd64.S") thirdparty_zstd_sources = [thirdparty_zstd_dir + file for file in thirdparty_zstd_sources] diff --git a/core/config/project_settings.cpp b/core/config/project_settings.cpp index 5b04986020..32f36e01f9 100644 --- a/core/config/project_settings.cpp +++ b/core/config/project_settings.cpp @@ -1472,10 +1472,6 @@ ProjectSettings::ProjectSettings() { GLOBAL_DEF(PropertyInfo(Variant::INT, "display/window/size/window_height_override", PROPERTY_HINT_RANGE, "0,4320,1,or_greater"), 0); // 8K resolution GLOBAL_DEF("display/window/energy_saving/keep_screen_on", true); -#ifdef TOOLS_ENABLED - GLOBAL_DEF("display/window/energy_saving/keep_screen_on.editor_hint", false); -#endif - GLOBAL_DEF("animation/warnings/check_invalid_track_paths", true); GLOBAL_DEF("animation/warnings/check_angle_interpolation_type_conflicting", true); diff --git a/platform/android/java/lib/src/org/godotengine/godot/io/file/FileErrors.kt b/core/core_bind.compat.inc index 2df0195de7..83b7b33e38 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/io/file/FileErrors.kt +++ b/core/core_bind.compat.inc @@ -1,5 +1,5 @@ /**************************************************************************/ -/* FileErrors.kt */ +/* core_bind.compat.inc */ /**************************************************************************/ /* This file is part of: */ /* GODOT ENGINE */ @@ -28,26 +28,18 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /**************************************************************************/ -package org.godotengine.godot.io.file +#ifndef DISABLE_DEPRECATED -/** - * Set of errors that may occur when performing data access. - */ -internal enum class FileErrors(val nativeValue: Int) { - OK(0), - FAILED(-1), - FILE_NOT_FOUND(-2), - FILE_CANT_OPEN(-3), - INVALID_PARAMETER(-4); +namespace core_bind { - companion object { - fun fromNativeError(error: Int): FileErrors? { - for (fileError in entries) { - if (fileError.nativeValue == error) { - return fileError - } - } - return null - } - } +void Semaphore::_post_bind_compat_93605() { + post(1); } + +void Semaphore::_bind_compatibility_methods() { + ClassDB::bind_compatibility_method(D_METHOD("post"), &Semaphore::_post_bind_compat_93605); +} + +}; // namespace core_bind + +#endif diff --git a/core/core_bind.cpp b/core/core_bind.cpp index 36f662b92b..4172793f9d 100644 --- a/core/core_bind.cpp +++ b/core/core_bind.cpp @@ -29,6 +29,7 @@ /**************************************************************************/ #include "core_bind.h" +#include "core_bind.compat.inc" #include "core/config/project_settings.h" #include "core/crypto/crypto_core.h" @@ -115,6 +116,11 @@ bool ResourceLoader::has_cached(const String &p_path) { return ResourceCache::has(local_path); } +Ref<Resource> ResourceLoader::get_cached_ref(const String &p_path) { + String local_path = ProjectSettings::get_singleton()->localize_path(p_path); + return ResourceCache::get_ref(local_path); +} + bool ResourceLoader::exists(const String &p_path, const String &p_type_hint) { return ::ResourceLoader::exists(p_path, p_type_hint); } @@ -135,6 +141,7 @@ void ResourceLoader::_bind_methods() { ClassDB::bind_method(D_METHOD("set_abort_on_missing_resources", "abort"), &ResourceLoader::set_abort_on_missing_resources); ClassDB::bind_method(D_METHOD("get_dependencies", "path"), &ResourceLoader::get_dependencies); ClassDB::bind_method(D_METHOD("has_cached", "path"), &ResourceLoader::has_cached); + ClassDB::bind_method(D_METHOD("get_cached_ref", "path"), &ResourceLoader::get_cached_ref); ClassDB::bind_method(D_METHOD("exists", "path", "type_hint"), &ResourceLoader::exists, DEFVAL("")); ClassDB::bind_method(D_METHOD("get_resource_uid", "path"), &ResourceLoader::get_resource_uid); @@ -1210,14 +1217,15 @@ bool Semaphore::try_wait() { return semaphore.try_wait(); } -void Semaphore::post() { - semaphore.post(); +void Semaphore::post(int p_count) { + ERR_FAIL_COND(p_count <= 0); + semaphore.post(p_count); } void Semaphore::_bind_methods() { ClassDB::bind_method(D_METHOD("wait"), &Semaphore::wait); ClassDB::bind_method(D_METHOD("try_wait"), &Semaphore::try_wait); - ClassDB::bind_method(D_METHOD("post"), &Semaphore::post); + ClassDB::bind_method(D_METHOD("post", "count"), &Semaphore::post, DEFVAL(1)); } ////// Mutex ////// @@ -1501,6 +1509,23 @@ TypedArray<Dictionary> ClassDB::class_get_method_list(const StringName &p_class, return ret; } +Variant ClassDB::class_call_static_method(const Variant **p_arguments, int p_argcount, Callable::CallError &r_call_error) { + if (p_argcount < 2) { + r_call_error.error = Callable::CallError::CALL_ERROR_TOO_FEW_ARGUMENTS; + return Variant::NIL; + } + if (!p_arguments[0]->is_string() || !p_arguments[1]->is_string()) { + r_call_error.error = Callable::CallError::CALL_ERROR_INVALID_ARGUMENT; + return Variant::NIL; + } + StringName class_ = *p_arguments[0]; + StringName method = *p_arguments[1]; + const MethodBind *bind = ::ClassDB::get_method(class_, method); + ERR_FAIL_NULL_V_MSG(bind, Variant::NIL, "Cannot find static method."); + ERR_FAIL_COND_V_MSG(!bind->is_static(), Variant::NIL, "Method is not static."); + return bind->call(nullptr, p_arguments + 2, p_argcount - 2, r_call_error); +} + PackedStringArray ClassDB::class_get_integer_constant_list(const StringName &p_class, bool p_no_inheritance) const { List<String> constants; ::ClassDB::get_integer_constant_list(p_class, &constants, p_no_inheritance); @@ -1623,6 +1648,8 @@ void ClassDB::_bind_methods() { ::ClassDB::bind_method(D_METHOD("class_get_method_list", "class", "no_inheritance"), &ClassDB::class_get_method_list, DEFVAL(false)); + ::ClassDB::bind_vararg_method(METHOD_FLAGS_DEFAULT, "class_call_static_method", &ClassDB::class_call_static_method, MethodInfo("class_call_static_method", PropertyInfo(Variant::STRING_NAME, "class"), PropertyInfo(Variant::STRING_NAME, "method"))); + ::ClassDB::bind_method(D_METHOD("class_get_integer_constant_list", "class", "no_inheritance"), &ClassDB::class_get_integer_constant_list, DEFVAL(false)); ::ClassDB::bind_method(D_METHOD("class_has_integer_constant", "class", "name"), &ClassDB::class_has_integer_constant); @@ -1749,7 +1776,7 @@ Object *Engine::get_singleton_object(const StringName &p_name) const { void Engine::register_singleton(const StringName &p_name, Object *p_object) { ERR_FAIL_COND_MSG(has_singleton(p_name), "Singleton already registered: " + String(p_name)); - ERR_FAIL_COND_MSG(!String(p_name).is_valid_identifier(), "Singleton name is not a valid identifier: " + p_name); + ERR_FAIL_COND_MSG(!String(p_name).is_valid_ascii_identifier(), "Singleton name is not a valid identifier: " + p_name); ::Engine::Singleton s; s.class_name = p_name; s.name = p_name; diff --git a/core/core_bind.h b/core/core_bind.h index d744da2551..122963e634 100644 --- a/core/core_bind.h +++ b/core/core_bind.h @@ -83,6 +83,7 @@ public: void set_abort_on_missing_resources(bool p_abort); PackedStringArray get_dependencies(const String &p_path); bool has_cached(const String &p_path); + Ref<Resource> get_cached_ref(const String &p_path); bool exists(const String &p_path, const String &p_type_hint = ""); ResourceUID::ID get_resource_uid(const String &p_path); @@ -390,12 +391,17 @@ class Semaphore : public RefCounted { GDCLASS(Semaphore, RefCounted); ::Semaphore semaphore; +protected: static void _bind_methods(); +#ifndef DISABLE_DEPRECATED + void _post_bind_compat_93605(); + static void _bind_compatibility_methods(); +#endif // DISABLE_DEPRECATED public: void wait(); bool try_wait(); - void post(); + void post(int p_count = 1); }; class Thread : public RefCounted { @@ -460,6 +466,7 @@ public: int class_get_method_argument_count(const StringName &p_class, const StringName &p_method, bool p_no_inheritance = false) const; TypedArray<Dictionary> class_get_method_list(const StringName &p_class, bool p_no_inheritance = false) const; + Variant class_call_static_method(const Variant **p_arguments, int p_argcount, Callable::CallError &r_call_error); PackedStringArray class_get_integer_constant_list(const StringName &p_class, bool p_no_inheritance = false) const; bool class_has_integer_constant(const StringName &p_class, const StringName &p_name) const; diff --git a/core/crypto/crypto.cpp b/core/crypto/crypto.cpp index d3d0079410..62bacadf91 100644 --- a/core/crypto/crypto.cpp +++ b/core/crypto/crypto.cpp @@ -36,10 +36,10 @@ /// Resources -CryptoKey *(*CryptoKey::_create)() = nullptr; -CryptoKey *CryptoKey::create() { +CryptoKey *(*CryptoKey::_create)(bool p_notify_postinitialize) = nullptr; +CryptoKey *CryptoKey::create(bool p_notify_postinitialize) { if (_create) { - return _create(); + return _create(p_notify_postinitialize); } return nullptr; } @@ -52,10 +52,10 @@ void CryptoKey::_bind_methods() { ClassDB::bind_method(D_METHOD("load_from_string", "string_key", "public_only"), &CryptoKey::load_from_string, DEFVAL(false)); } -X509Certificate *(*X509Certificate::_create)() = nullptr; -X509Certificate *X509Certificate::create() { +X509Certificate *(*X509Certificate::_create)(bool p_notify_postinitialize) = nullptr; +X509Certificate *X509Certificate::create(bool p_notify_postinitialize) { if (_create) { - return _create(); + return _create(p_notify_postinitialize); } return nullptr; } @@ -116,10 +116,10 @@ void HMACContext::_bind_methods() { ClassDB::bind_method(D_METHOD("finish"), &HMACContext::finish); } -HMACContext *(*HMACContext::_create)() = nullptr; -HMACContext *HMACContext::create() { +HMACContext *(*HMACContext::_create)(bool p_notify_postinitialize) = nullptr; +HMACContext *HMACContext::create(bool p_notify_postinitialize) { if (_create) { - return _create(); + return _create(p_notify_postinitialize); } ERR_FAIL_V_MSG(nullptr, "HMACContext is not available when the mbedtls module is disabled."); } @@ -127,10 +127,10 @@ HMACContext *HMACContext::create() { /// Crypto void (*Crypto::_load_default_certificates)(const String &p_path) = nullptr; -Crypto *(*Crypto::_create)() = nullptr; -Crypto *Crypto::create() { +Crypto *(*Crypto::_create)(bool p_notify_postinitialize) = nullptr; +Crypto *Crypto::create(bool p_notify_postinitialize) { if (_create) { - return _create(); + return _create(p_notify_postinitialize); } ERR_FAIL_V_MSG(nullptr, "Crypto is not available when the mbedtls module is disabled."); } diff --git a/core/crypto/crypto.h b/core/crypto/crypto.h index 16649422cf..c19e6b6773 100644 --- a/core/crypto/crypto.h +++ b/core/crypto/crypto.h @@ -42,10 +42,10 @@ class CryptoKey : public Resource { protected: static void _bind_methods(); - static CryptoKey *(*_create)(); + static CryptoKey *(*_create)(bool p_notify_postinitialize); public: - static CryptoKey *create(); + static CryptoKey *create(bool p_notify_postinitialize = true); virtual Error load(const String &p_path, bool p_public_only = false) = 0; virtual Error save(const String &p_path, bool p_public_only = false) = 0; virtual String save_to_string(bool p_public_only = false) = 0; @@ -58,10 +58,10 @@ class X509Certificate : public Resource { protected: static void _bind_methods(); - static X509Certificate *(*_create)(); + static X509Certificate *(*_create)(bool p_notify_postinitialize); public: - static X509Certificate *create(); + static X509Certificate *create(bool p_notify_postinitialize = true); virtual Error load(const String &p_path) = 0; virtual Error load_from_memory(const uint8_t *p_buffer, int p_len) = 0; virtual Error save(const String &p_path) = 0; @@ -106,10 +106,10 @@ class HMACContext : public RefCounted { protected: static void _bind_methods(); - static HMACContext *(*_create)(); + static HMACContext *(*_create)(bool p_notify_postinitialize); public: - static HMACContext *create(); + static HMACContext *create(bool p_notify_postinitialize = true); virtual Error start(HashingContext::HashType p_hash_type, const PackedByteArray &p_key) = 0; virtual Error update(const PackedByteArray &p_data) = 0; @@ -124,11 +124,11 @@ class Crypto : public RefCounted { protected: static void _bind_methods(); - static Crypto *(*_create)(); + static Crypto *(*_create)(bool p_notify_postinitialize); static void (*_load_default_certificates)(const String &p_path); public: - static Crypto *create(); + static Crypto *create(bool p_notify_postinitialize = true); static void load_default_certificates(const String &p_path); virtual PackedByteArray generate_random_bytes(int p_bytes) = 0; diff --git a/core/debugger/remote_debugger_peer.cpp b/core/debugger/remote_debugger_peer.cpp index 21a9014626..9dca47a0b4 100644 --- a/core/debugger/remote_debugger_peer.cpp +++ b/core/debugger/remote_debugger_peer.cpp @@ -144,9 +144,8 @@ void RemoteDebuggerPeerTCP::_read_in() { Error err = decode_variant(var, buf, in_pos, &read); ERR_CONTINUE(read != in_pos || err != OK); ERR_CONTINUE_MSG(var.get_type() != Variant::ARRAY, "Malformed packet received, not an Array."); - mutex.lock(); + MutexLock lock(mutex); in_queue.push_back(var); - mutex.unlock(); } } } diff --git a/core/error/error_list.h b/core/error/error_list.h index abc637106a..cdf06eb06d 100644 --- a/core/error/error_list.h +++ b/core/error/error_list.h @@ -41,6 +41,7 @@ * - Are added to the Error enum in core/error/error_list.h * - Have a description added to error_names in core/error/error_list.cpp * - Are bound with BIND_CORE_ENUM_CONSTANT() in core/core_constants.cpp + * - Have a matching Android version in platform/android/java/lib/src/org/godotengine/godot/error/Error.kt */ enum Error { diff --git a/core/extension/gdextension.cpp b/core/extension/gdextension.cpp index cb6832ea39..e764b9c112 100644 --- a/core/extension/gdextension.cpp +++ b/core/extension/gdextension.cpp @@ -32,11 +32,9 @@ #include "gdextension.compat.inc" #include "core/config/project_settings.h" -#include "core/io/dir_access.h" #include "core/object/class_db.h" #include "core/object/method_bind.h" -#include "core/os/os.h" -#include "core/version.h" +#include "gdextension_library_loader.h" #include "gdextension_manager.h" extern void gdextension_setup_interface(); @@ -48,146 +46,6 @@ String GDExtension::get_extension_list_config_file() { return ProjectSettings::get_singleton()->get_project_data_path().path_join("extension_list.cfg"); } -Vector<SharedObject> GDExtension::find_extension_dependencies(const String &p_path, Ref<ConfigFile> p_config, std::function<bool(String)> p_has_feature) { - Vector<SharedObject> dependencies_shared_objects; - if (p_config->has_section("dependencies")) { - List<String> config_dependencies; - p_config->get_section_keys("dependencies", &config_dependencies); - - for (const String &dependency : config_dependencies) { - Vector<String> dependency_tags = dependency.split("."); - bool all_tags_met = true; - for (int i = 0; i < dependency_tags.size(); i++) { - String tag = dependency_tags[i].strip_edges(); - if (!p_has_feature(tag)) { - all_tags_met = false; - break; - } - } - - if (all_tags_met) { - Dictionary dependency_value = p_config->get_value("dependencies", dependency); - for (const Variant *key = dependency_value.next(nullptr); key; key = dependency_value.next(key)) { - String dependency_path = *key; - String target_path = dependency_value[*key]; - if (dependency_path.is_relative_path()) { - dependency_path = p_path.get_base_dir().path_join(dependency_path); - } - dependencies_shared_objects.push_back(SharedObject(dependency_path, dependency_tags, target_path)); - } - break; - } - } - } - - return dependencies_shared_objects; -} - -String GDExtension::find_extension_library(const String &p_path, Ref<ConfigFile> p_config, std::function<bool(String)> p_has_feature, PackedStringArray *r_tags) { - // First, check the explicit libraries. - if (p_config->has_section("libraries")) { - List<String> libraries; - p_config->get_section_keys("libraries", &libraries); - - // Iterate the libraries, finding the best matching tags. - String best_library_path; - Vector<String> best_library_tags; - for (const String &E : libraries) { - Vector<String> tags = E.split("."); - bool all_tags_met = true; - for (int i = 0; i < tags.size(); i++) { - String tag = tags[i].strip_edges(); - if (!p_has_feature(tag)) { - all_tags_met = false; - break; - } - } - - if (all_tags_met && tags.size() > best_library_tags.size()) { - best_library_path = p_config->get_value("libraries", E); - best_library_tags = tags; - } - } - - if (!best_library_path.is_empty()) { - if (best_library_path.is_relative_path()) { - best_library_path = p_path.get_base_dir().path_join(best_library_path); - } - if (r_tags != nullptr) { - r_tags->append_array(best_library_tags); - } - return best_library_path; - } - } - - // Second, try to autodetect - String autodetect_library_prefix; - if (p_config->has_section_key("configuration", "autodetect_library_prefix")) { - autodetect_library_prefix = p_config->get_value("configuration", "autodetect_library_prefix"); - } - if (!autodetect_library_prefix.is_empty()) { - String autodetect_path = autodetect_library_prefix; - if (autodetect_path.is_relative_path()) { - autodetect_path = p_path.get_base_dir().path_join(autodetect_path); - } - - // Find the folder and file parts of the prefix. - String folder; - String file_prefix; - if (DirAccess::dir_exists_absolute(autodetect_path)) { - folder = autodetect_path; - } else if (DirAccess::dir_exists_absolute(autodetect_path.get_base_dir())) { - folder = autodetect_path.get_base_dir(); - file_prefix = autodetect_path.get_file(); - } else { - ERR_FAIL_V_MSG(String(), vformat("Error in extension: %s. Could not find folder for automatic detection of libraries files. autodetect_library_prefix=\"%s\"", p_path, autodetect_library_prefix)); - } - - // Open the folder. - Ref<DirAccess> dir = DirAccess::open(folder); - ERR_FAIL_COND_V_MSG(!dir.is_valid(), String(), vformat("Error in extension: %s. Could not open folder for automatic detection of libraries files. autodetect_library_prefix=\"%s\"", p_path, autodetect_library_prefix)); - - // Iterate the files and check the prefixes, finding the best matching file. - String best_file; - Vector<String> best_file_tags; - dir->list_dir_begin(); - String file_name = dir->_get_next(); - while (file_name != "") { - if (!dir->current_is_dir() && file_name.begins_with(file_prefix)) { - // Check if the files matches all requested feature tags. - String tags_str = file_name.trim_prefix(file_prefix); - tags_str = tags_str.trim_suffix(tags_str.get_extension()); - - Vector<String> tags = tags_str.split(".", false); - bool all_tags_met = true; - for (int i = 0; i < tags.size(); i++) { - String tag = tags[i].strip_edges(); - if (!p_has_feature(tag)) { - all_tags_met = false; - break; - } - } - - // If all tags are found in the feature list, and we found more tags than before, use this file. - if (all_tags_met && tags.size() > best_file_tags.size()) { - best_file_tags = tags; - best_file = file_name; - } - } - file_name = dir->_get_next(); - } - - if (!best_file.is_empty()) { - String library_path = folder.path_join(best_file); - if (r_tags != nullptr) { - r_tags->append_array(best_file_tags); - } - return library_path; - } - } - return String(); -} - class GDExtensionMethodBind : public MethodBind { GDExtensionClassMethodCall call_func; GDExtensionClassMethodValidatedCall validated_call_func; @@ -382,7 +240,7 @@ public: #ifndef DISABLE_DEPRECATED void GDExtension::_register_extension_class(GDExtensionClassLibraryPtr p_library, GDExtensionConstStringNamePtr p_class_name, GDExtensionConstStringNamePtr p_parent_class_name, const GDExtensionClassCreationInfo *p_extension_funcs) { - const GDExtensionClassCreationInfo3 class_info3 = { + const GDExtensionClassCreationInfo4 class_info4 = { p_extension_funcs->is_virtual, // GDExtensionBool is_virtual; p_extension_funcs->is_abstract, // GDExtensionBool is_abstract; true, // GDExtensionBool is_exposed; @@ -398,7 +256,7 @@ void GDExtension::_register_extension_class(GDExtensionClassLibraryPtr p_library p_extension_funcs->to_string_func, // GDExtensionClassToString to_string_func; p_extension_funcs->reference_func, // GDExtensionClassReference reference_func; p_extension_funcs->unreference_func, // GDExtensionClassUnreference unreference_func; - p_extension_funcs->create_instance_func, // GDExtensionClassCreateInstance create_instance_func; /* this one is mandatory */ + nullptr, // GDExtensionClassCreateInstance2 create_instance_func; /* this one is mandatory */ p_extension_funcs->free_instance_func, // GDExtensionClassFreeInstance free_instance_func; /* this one is mandatory */ nullptr, // GDExtensionClassRecreateInstance recreate_instance_func; p_extension_funcs->get_virtual_func, // GDExtensionClassGetVirtual get_virtual_func; @@ -411,12 +269,13 @@ void GDExtension::_register_extension_class(GDExtensionClassLibraryPtr p_library const ClassCreationDeprecatedInfo legacy = { p_extension_funcs->notification_func, // GDExtensionClassNotification notification_func; p_extension_funcs->free_property_list_func, // GDExtensionClassFreePropertyList free_property_list_func; + p_extension_funcs->create_instance_func, // GDExtensionClassCreateInstance create_instance_func; }; - _register_extension_class_internal(p_library, p_class_name, p_parent_class_name, &class_info3, &legacy); + _register_extension_class_internal(p_library, p_class_name, p_parent_class_name, &class_info4, &legacy); } void GDExtension::_register_extension_class2(GDExtensionClassLibraryPtr p_library, GDExtensionConstStringNamePtr p_class_name, GDExtensionConstStringNamePtr p_parent_class_name, const GDExtensionClassCreationInfo2 *p_extension_funcs) { - const GDExtensionClassCreationInfo3 class_info3 = { + const GDExtensionClassCreationInfo4 class_info4 = { p_extension_funcs->is_virtual, // GDExtensionBool is_virtual; p_extension_funcs->is_abstract, // GDExtensionBool is_abstract; p_extension_funcs->is_exposed, // GDExtensionBool is_exposed; @@ -432,7 +291,7 @@ void GDExtension::_register_extension_class2(GDExtensionClassLibraryPtr p_librar p_extension_funcs->to_string_func, // GDExtensionClassToString to_string_func; p_extension_funcs->reference_func, // GDExtensionClassReference reference_func; p_extension_funcs->unreference_func, // GDExtensionClassUnreference unreference_func; - p_extension_funcs->create_instance_func, // GDExtensionClassCreateInstance create_instance_func; /* this one is mandatory */ + nullptr, // GDExtensionClassCreateInstance2 create_instance_func; /* this one is mandatory */ p_extension_funcs->free_instance_func, // GDExtensionClassFreeInstance free_instance_func; /* this one is mandatory */ p_extension_funcs->recreate_instance_func, // GDExtensionClassRecreateInstance recreate_instance_func; p_extension_funcs->get_virtual_func, // GDExtensionClassGetVirtual get_virtual_func; @@ -445,21 +304,58 @@ void GDExtension::_register_extension_class2(GDExtensionClassLibraryPtr p_librar const ClassCreationDeprecatedInfo legacy = { nullptr, // GDExtensionClassNotification notification_func; p_extension_funcs->free_property_list_func, // GDExtensionClassFreePropertyList free_property_list_func; + p_extension_funcs->create_instance_func, // GDExtensionClassCreateInstance create_instance_func; }; - _register_extension_class_internal(p_library, p_class_name, p_parent_class_name, &class_info3, &legacy); + _register_extension_class_internal(p_library, p_class_name, p_parent_class_name, &class_info4, &legacy); } -#endif // DISABLE_DEPRECATED void GDExtension::_register_extension_class3(GDExtensionClassLibraryPtr p_library, GDExtensionConstStringNamePtr p_class_name, GDExtensionConstStringNamePtr p_parent_class_name, const GDExtensionClassCreationInfo3 *p_extension_funcs) { + const GDExtensionClassCreationInfo4 class_info4 = { + p_extension_funcs->is_virtual, // GDExtensionBool is_virtual; + p_extension_funcs->is_abstract, // GDExtensionBool is_abstract; + p_extension_funcs->is_exposed, // GDExtensionBool is_exposed; + p_extension_funcs->is_runtime, // GDExtensionBool is_runtime; + p_extension_funcs->set_func, // GDExtensionClassSet set_func; + p_extension_funcs->get_func, // GDExtensionClassGet get_func; + p_extension_funcs->get_property_list_func, // GDExtensionClassGetPropertyList get_property_list_func; + p_extension_funcs->free_property_list_func, // GDExtensionClassFreePropertyList free_property_list_func; + p_extension_funcs->property_can_revert_func, // GDExtensionClassPropertyCanRevert property_can_revert_func; + p_extension_funcs->property_get_revert_func, // GDExtensionClassPropertyGetRevert property_get_revert_func; + p_extension_funcs->validate_property_func, // GDExtensionClassValidateProperty validate_property_func; + p_extension_funcs->notification_func, // GDExtensionClassNotification2 notification_func; + p_extension_funcs->to_string_func, // GDExtensionClassToString to_string_func; + p_extension_funcs->reference_func, // GDExtensionClassReference reference_func; + p_extension_funcs->unreference_func, // GDExtensionClassUnreference unreference_func; + nullptr, // GDExtensionClassCreateInstance2 create_instance_func; /* this one is mandatory */ + p_extension_funcs->free_instance_func, // GDExtensionClassFreeInstance free_instance_func; /* this one is mandatory */ + p_extension_funcs->recreate_instance_func, // GDExtensionClassRecreateInstance recreate_instance_func; + p_extension_funcs->get_virtual_func, // GDExtensionClassGetVirtual get_virtual_func; + p_extension_funcs->get_virtual_call_data_func, // GDExtensionClassGetVirtualCallData get_virtual_call_data_func; + p_extension_funcs->call_virtual_with_data_func, // GDExtensionClassCallVirtualWithData call_virtual_func; + p_extension_funcs->get_rid_func, // GDExtensionClassGetRID get_rid; + p_extension_funcs->class_userdata, // void *class_userdata; + }; + + const ClassCreationDeprecatedInfo legacy = { + nullptr, // GDExtensionClassNotification notification_func; + nullptr, // GDExtensionClassFreePropertyList free_property_list_func; + p_extension_funcs->create_instance_func, // GDExtensionClassCreateInstance2 create_instance_func; + }; + _register_extension_class_internal(p_library, p_class_name, p_parent_class_name, &class_info4, &legacy); +} + +#endif // DISABLE_DEPRECATED + +void GDExtension::_register_extension_class4(GDExtensionClassLibraryPtr p_library, GDExtensionConstStringNamePtr p_class_name, GDExtensionConstStringNamePtr p_parent_class_name, const GDExtensionClassCreationInfo4 *p_extension_funcs) { _register_extension_class_internal(p_library, p_class_name, p_parent_class_name, p_extension_funcs); } -void GDExtension::_register_extension_class_internal(GDExtensionClassLibraryPtr p_library, GDExtensionConstStringNamePtr p_class_name, GDExtensionConstStringNamePtr p_parent_class_name, const GDExtensionClassCreationInfo3 *p_extension_funcs, const ClassCreationDeprecatedInfo *p_deprecated_funcs) { +void GDExtension::_register_extension_class_internal(GDExtensionClassLibraryPtr p_library, GDExtensionConstStringNamePtr p_class_name, GDExtensionConstStringNamePtr p_parent_class_name, const GDExtensionClassCreationInfo4 *p_extension_funcs, const ClassCreationDeprecatedInfo *p_deprecated_funcs) { GDExtension *self = reinterpret_cast<GDExtension *>(p_library); StringName class_name = *reinterpret_cast<const StringName *>(p_class_name); StringName parent_class_name = *reinterpret_cast<const StringName *>(p_parent_class_name); - ERR_FAIL_COND_MSG(!String(class_name).is_valid_identifier(), "Attempt to register extension class '" + class_name + "', which is not a valid class identifier."); + ERR_FAIL_COND_MSG(!String(class_name).is_valid_ascii_identifier(), "Attempt to register extension class '" + class_name + "', which is not a valid class identifier."); ERR_FAIL_COND_MSG(ClassDB::class_exists(class_name), "Attempt to register extension class '" + class_name + "', which appears to be already registered."); Extension *parent_extension = nullptr; @@ -530,6 +426,7 @@ void GDExtension::_register_extension_class_internal(GDExtensionClassLibraryPtr if (p_deprecated_funcs) { extension->gdextension.notification = p_deprecated_funcs->notification_func; extension->gdextension.free_property_list = p_deprecated_funcs->free_property_list_func; + extension->gdextension.create_instance = p_deprecated_funcs->create_instance_func; } #endif // DISABLE_DEPRECATED extension->gdextension.notification2 = p_extension_funcs->notification_func; @@ -537,7 +434,7 @@ void GDExtension::_register_extension_class_internal(GDExtensionClassLibraryPtr extension->gdextension.reference = p_extension_funcs->reference_func; extension->gdextension.unreference = p_extension_funcs->unreference_func; extension->gdextension.class_userdata = p_extension_funcs->class_userdata; - extension->gdextension.create_instance = p_extension_funcs->create_instance_func; + extension->gdextension.create_instance2 = p_extension_funcs->create_instance_func; extension->gdextension.free_instance = p_extension_funcs->free_instance_func; extension->gdextension.recreate_instance = p_extension_funcs->recreate_instance_func; extension->gdextension.get_virtual = p_extension_funcs->get_virtual_func; @@ -755,7 +652,13 @@ void GDExtension::_unregister_extension_class(GDExtensionClassLibraryPtr p_libra void GDExtension::_get_library_path(GDExtensionClassLibraryPtr p_library, GDExtensionUninitializedStringPtr r_path) { GDExtension *self = reinterpret_cast<GDExtension *>(p_library); - memnew_placement(r_path, String(self->library_path)); + Ref<GDExtensionLibraryLoader> library_loader = self->loader; + String library_path; + if (library_loader.is_valid()) { + library_path = library_loader->library_path; + } + + memnew_placement(r_path, String(library_path)); } HashMap<StringName, GDExtensionInterfaceFunctionPtr> GDExtension::gdextension_interface_functions; @@ -771,55 +674,32 @@ GDExtensionInterfaceFunctionPtr GDExtension::get_interface_function(const String return *function; } -Error GDExtension::open_library(const String &p_path, const String &p_entry_symbol, Vector<SharedObject> *p_dependencies) { - String abs_path = ProjectSettings::get_singleton()->globalize_path(p_path); +Error GDExtension::open_library(const String &p_path, const Ref<GDExtensionLoader> &p_loader) { + ERR_FAIL_NULL_V_MSG(p_loader, FAILED, "Can't open GDExtension without a loader."); + loader = p_loader; - Vector<String> abs_dependencies_paths; - if (p_dependencies != nullptr && !p_dependencies->is_empty()) { - for (const SharedObject &dependency : *p_dependencies) { - abs_dependencies_paths.push_back(ProjectSettings::get_singleton()->globalize_path(dependency.path)); - } - } + Error err = loader->open_library(p_path); - OS::GDExtensionData data = { - true, // also_set_library_path - &library_path, // r_resolved_path - Engine::get_singleton()->is_editor_hint(), // generate_temp_files - &abs_dependencies_paths, // library_dependencies - }; - Error err = OS::get_singleton()->open_dynamic_library(abs_path, library, &data); + ERR_FAIL_COND_V_MSG(err == ERR_FILE_NOT_FOUND, err, "GDExtension dynamic library not found: " + p_path); + ERR_FAIL_COND_V_MSG(err != OK, err, "Can't open GDExtension dynamic library: " + p_path); - ERR_FAIL_COND_V_MSG(err == ERR_FILE_NOT_FOUND, err, "GDExtension dynamic library not found: " + abs_path); - ERR_FAIL_COND_V_MSG(err != OK, err, "Can't open GDExtension dynamic library: " + abs_path); - - void *entry_funcptr = nullptr; - - err = OS::get_singleton()->get_dynamic_library_symbol_handle(library, p_entry_symbol, entry_funcptr, false); + err = loader->initialize(&gdextension_get_proc_address, this, &initialization); if (err != OK) { - ERR_PRINT("GDExtension entry point '" + p_entry_symbol + "' not found in library " + abs_path); - OS::get_singleton()->close_dynamic_library(library); + // Errors already logged in initialize(). + loader->close_library(); return err; } - GDExtensionInitializationFunction initialization_function = (GDExtensionInitializationFunction)entry_funcptr; - GDExtensionBool ret = initialization_function(&gdextension_get_proc_address, this, &initialization); + level_initialized = -1; - if (ret) { - level_initialized = -1; - return OK; - } else { - ERR_PRINT("GDExtension initialization function '" + p_entry_symbol + "' returned an error."); - OS::get_singleton()->close_dynamic_library(library); - return FAILED; - } + return OK; } void GDExtension::close_library() { - ERR_FAIL_NULL(library); - OS::get_singleton()->close_dynamic_library(library); + ERR_FAIL_COND(!is_library_open()); + loader->close_library(); - library = nullptr; class_icon_paths.clear(); #ifdef TOOLS_ENABLED @@ -828,16 +708,16 @@ void GDExtension::close_library() { } bool GDExtension::is_library_open() const { - return library != nullptr; + return loader.is_valid() && loader->is_library_open(); } GDExtension::InitializationLevel GDExtension::get_minimum_library_initialization_level() const { - ERR_FAIL_NULL_V(library, INITIALIZATION_LEVEL_CORE); + ERR_FAIL_COND_V(!is_library_open(), INITIALIZATION_LEVEL_CORE); return InitializationLevel(initialization.minimum_initialization_level); } void GDExtension::initialize_library(InitializationLevel p_level) { - ERR_FAIL_NULL(library); + ERR_FAIL_COND(!is_library_open()); ERR_FAIL_COND_MSG(p_level <= int32_t(level_initialized), vformat("Level '%d' must be higher than the current level '%d'", p_level, level_initialized)); level_initialized = int32_t(p_level); @@ -847,7 +727,7 @@ void GDExtension::initialize_library(InitializationLevel p_level) { initialization.initialize(initialization.userdata, GDExtensionInitializationLevel(p_level)); } void GDExtension::deinitialize_library(InitializationLevel p_level) { - ERR_FAIL_NULL(library); + ERR_FAIL_COND(!is_library_open()); ERR_FAIL_COND(p_level > int32_t(level_initialized)); level_initialized = int32_t(p_level) - 1; @@ -871,7 +751,7 @@ GDExtension::GDExtension() { } GDExtension::~GDExtension() { - if (library != nullptr) { + if (is_library_open()) { close_library(); } #ifdef TOOLS_ENABLED @@ -888,8 +768,9 @@ void GDExtension::initialize_gdextensions() { #ifndef DISABLE_DEPRECATED register_interface_function("classdb_register_extension_class", (GDExtensionInterfaceFunctionPtr)&GDExtension::_register_extension_class); register_interface_function("classdb_register_extension_class2", (GDExtensionInterfaceFunctionPtr)&GDExtension::_register_extension_class2); -#endif // DISABLE_DEPRECATED register_interface_function("classdb_register_extension_class3", (GDExtensionInterfaceFunctionPtr)&GDExtension::_register_extension_class3); +#endif // DISABLE_DEPRECATED + register_interface_function("classdb_register_extension_class4", (GDExtensionInterfaceFunctionPtr)&GDExtension::_register_extension_class4); register_interface_function("classdb_register_extension_class_method", (GDExtensionInterfaceFunctionPtr)&GDExtension::_register_extension_class_method); register_interface_function("classdb_register_extension_class_virtual_method", (GDExtensionInterfaceFunctionPtr)&GDExtension::_register_extension_class_virtual_method); register_interface_function("classdb_register_extension_class_integer_constant", (GDExtensionInterfaceFunctionPtr)&GDExtension::_register_extension_class_integer_constant); @@ -909,142 +790,15 @@ void GDExtension::finalize_gdextensions() { Error GDExtensionResourceLoader::load_gdextension_resource(const String &p_path, Ref<GDExtension> &p_extension) { ERR_FAIL_COND_V_MSG(p_extension.is_valid() && p_extension->is_library_open(), ERR_ALREADY_IN_USE, "Cannot load GDExtension resource into already opened library."); - Ref<ConfigFile> config; - config.instantiate(); - - Error err = config->load(p_path); - - if (err != OK) { - ERR_PRINT("Error loading GDExtension configuration file: " + p_path); - return err; - } + GDExtensionManager *extension_manager = GDExtensionManager::get_singleton(); - if (!config->has_section_key("configuration", "entry_symbol")) { - ERR_PRINT("GDExtension configuration file must contain a \"configuration/entry_symbol\" key: " + p_path); - return ERR_INVALID_DATA; - } - - String entry_symbol = config->get_value("configuration", "entry_symbol"); - - uint32_t compatibility_minimum[3] = { 0, 0, 0 }; - if (config->has_section_key("configuration", "compatibility_minimum")) { - String compat_string = config->get_value("configuration", "compatibility_minimum"); - Vector<int> parts = compat_string.split_ints("."); - for (int i = 0; i < parts.size(); i++) { - if (i >= 3) { - break; - } - if (parts[i] >= 0) { - compatibility_minimum[i] = parts[i]; - } - } - } else { - ERR_PRINT("GDExtension configuration file must contain a \"configuration/compatibility_minimum\" key: " + p_path); - return ERR_INVALID_DATA; - } - - if (compatibility_minimum[0] < 4 || (compatibility_minimum[0] == 4 && compatibility_minimum[1] == 0)) { - ERR_PRINT(vformat("GDExtension's compatibility_minimum (%d.%d.%d) must be at least 4.1.0: %s", compatibility_minimum[0], compatibility_minimum[1], compatibility_minimum[2], p_path)); - return ERR_INVALID_DATA; - } - - bool compatible = true; - // Check version lexicographically. - if (VERSION_MAJOR != compatibility_minimum[0]) { - compatible = VERSION_MAJOR > compatibility_minimum[0]; - } else if (VERSION_MINOR != compatibility_minimum[1]) { - compatible = VERSION_MINOR > compatibility_minimum[1]; - } else { - compatible = VERSION_PATCH >= compatibility_minimum[2]; - } - if (!compatible) { - ERR_PRINT(vformat("GDExtension only compatible with Godot version %d.%d.%d or later: %s", compatibility_minimum[0], compatibility_minimum[1], compatibility_minimum[2], p_path)); - return ERR_INVALID_DATA; - } - - // Optionally check maximum compatibility. - if (config->has_section_key("configuration", "compatibility_maximum")) { - uint32_t compatibility_maximum[3] = { 0, 0, 0 }; - String compat_string = config->get_value("configuration", "compatibility_maximum"); - Vector<int> parts = compat_string.split_ints("."); - for (int i = 0; i < 3; i++) { - if (i < parts.size() && parts[i] >= 0) { - compatibility_maximum[i] = parts[i]; - } else { - // If a version part is missing, set the maximum to an arbitrary high value. - compatibility_maximum[i] = 9999; - } - } - - compatible = true; - if (VERSION_MAJOR != compatibility_maximum[0]) { - compatible = VERSION_MAJOR < compatibility_maximum[0]; - } else if (VERSION_MINOR != compatibility_maximum[1]) { - compatible = VERSION_MINOR < compatibility_maximum[1]; - } -#if VERSION_PATCH - // #if check to avoid -Wtype-limits warning when 0. - else { - compatible = VERSION_PATCH <= compatibility_maximum[2]; - } -#endif - - if (!compatible) { - ERR_PRINT(vformat("GDExtension only compatible with Godot version %s or earlier: %s", compat_string, p_path)); - return ERR_INVALID_DATA; - } - } - - String library_path = GDExtension::find_extension_library(p_path, config, [](const String &p_feature) { return OS::get_singleton()->has_feature(p_feature); }); - - if (library_path.is_empty()) { - const String os_arch = OS::get_singleton()->get_name().to_lower() + "." + Engine::get_singleton()->get_architecture_name(); - ERR_PRINT(vformat("No GDExtension library found for current OS and architecture (%s) in configuration file: %s", os_arch, p_path)); - return ERR_FILE_NOT_FOUND; - } - - bool is_static_library = library_path.ends_with(".a") || library_path.ends_with(".xcframework"); - - if (!library_path.is_resource_file() && !library_path.is_absolute_path()) { - library_path = p_path.get_base_dir().path_join(library_path); - } - - if (p_extension.is_null()) { - p_extension.instantiate(); - } - -#ifdef TOOLS_ENABLED - p_extension->set_reloadable(config->get_value("configuration", "reloadable", false) && Engine::get_singleton()->is_extension_reloading_enabled()); - - p_extension->update_last_modified_time( - FileAccess::get_modified_time(p_path), - FileAccess::get_modified_time(library_path)); -#endif - - Vector<SharedObject> library_dependencies = GDExtension::find_extension_dependencies(p_path, config, [](const String &p_feature) { return OS::get_singleton()->has_feature(p_feature); }); - err = p_extension->open_library(is_static_library ? String() : library_path, entry_symbol, &library_dependencies); - if (err != OK) { - // Unreference the extension so that this loading can be considered a failure. - p_extension.unref(); - - // Errors already logged in open_library() - return err; - } - - // Handle icons if any are specified. - if (config->has_section("icons")) { - List<String> keys; - config->get_section_keys("icons", &keys); - for (const String &key : keys) { - String icon_path = config->get_value("icons", key); - if (icon_path.is_relative_path()) { - icon_path = p_path.get_base_dir().path_join(icon_path); - } - - p_extension->class_icon_paths[key] = icon_path; - } + GDExtensionManager::LoadStatus status = extension_manager->load_extension(p_path); + if (status != GDExtensionManager::LOAD_STATUS_OK && status != GDExtensionManager::LOAD_STATUS_ALREADY_LOADED) { + // Errors already logged in load_extension(). + return FAILED; } + p_extension = extension_manager->get_extension(p_path); return OK; } @@ -1085,16 +839,7 @@ String GDExtensionResourceLoader::get_resource_type(const String &p_path) const #ifdef TOOLS_ENABLED bool GDExtension::has_library_changed() const { - // Check only that the last modified time is different (rather than checking - // that it's newer) since some OS's (namely Windows) will preserve the modified - // time by default when copying files. - if (FileAccess::get_modified_time(get_path()) != resource_last_modified_time) { - return true; - } - if (FileAccess::get_modified_time(library_path) != library_last_modified_time) { - return true; - } - return false; + return loader->has_library_changed(); } void GDExtension::prepare_reload() { diff --git a/core/extension/gdextension.h b/core/extension/gdextension.h index 9393e7399b..7bb4294909 100644 --- a/core/extension/gdextension.h +++ b/core/extension/gdextension.h @@ -31,13 +31,11 @@ #ifndef GDEXTENSION_H #define GDEXTENSION_H -#include <functional> - #include "core/extension/gdextension_interface.h" +#include "core/extension/gdextension_loader.h" #include "core/io/config_file.h" #include "core/io/resource_loader.h" #include "core/object/ref_counted.h" -#include "core/os/shared_object.h" class GDExtensionMethodBind; @@ -46,8 +44,8 @@ class GDExtension : public Resource { friend class GDExtensionManager; - void *library = nullptr; // pointer if valid, - String library_path; + Ref<GDExtensionLoader> loader; + bool reloadable = false; struct Extension { @@ -72,15 +70,17 @@ class GDExtension : public Resource { #ifndef DISABLE_DEPRECATED GDExtensionClassNotification notification_func = nullptr; GDExtensionClassFreePropertyList free_property_list_func = nullptr; + GDExtensionClassCreateInstance create_instance_func = nullptr; #endif // DISABLE_DEPRECATED }; #ifndef DISABLE_DEPRECATED static void _register_extension_class(GDExtensionClassLibraryPtr p_library, GDExtensionConstStringNamePtr p_class_name, GDExtensionConstStringNamePtr p_parent_class_name, const GDExtensionClassCreationInfo *p_extension_funcs); static void _register_extension_class2(GDExtensionClassLibraryPtr p_library, GDExtensionConstStringNamePtr p_class_name, GDExtensionConstStringNamePtr p_parent_class_name, const GDExtensionClassCreationInfo2 *p_extension_funcs); -#endif // DISABLE_DEPRECATED static void _register_extension_class3(GDExtensionClassLibraryPtr p_library, GDExtensionConstStringNamePtr p_class_name, GDExtensionConstStringNamePtr p_parent_class_name, const GDExtensionClassCreationInfo3 *p_extension_funcs); - static void _register_extension_class_internal(GDExtensionClassLibraryPtr p_library, GDExtensionConstStringNamePtr p_class_name, GDExtensionConstStringNamePtr p_parent_class_name, const GDExtensionClassCreationInfo3 *p_extension_funcs, const ClassCreationDeprecatedInfo *p_deprecated_funcs = nullptr); +#endif // DISABLE_DEPRECATED + static void _register_extension_class4(GDExtensionClassLibraryPtr p_library, GDExtensionConstStringNamePtr p_class_name, GDExtensionConstStringNamePtr p_parent_class_name, const GDExtensionClassCreationInfo4 *p_extension_funcs); + static void _register_extension_class_internal(GDExtensionClassLibraryPtr p_library, GDExtensionConstStringNamePtr p_class_name, GDExtensionConstStringNamePtr p_parent_class_name, const GDExtensionClassCreationInfo4 *p_extension_funcs, const ClassCreationDeprecatedInfo *p_deprecated_funcs = nullptr); static void _register_extension_class_method(GDExtensionClassLibraryPtr p_library, GDExtensionConstStringNamePtr p_class_name, const GDExtensionClassMethodInfo *p_method_info); static void _register_extension_class_virtual_method(GDExtensionClassLibraryPtr p_library, GDExtensionConstStringNamePtr p_class_name, const GDExtensionClassVirtualMethodInfo *p_method_info); static void _register_extension_class_integer_constant(GDExtensionClassLibraryPtr p_library, GDExtensionConstStringNamePtr p_class_name, GDExtensionConstStringNamePtr p_enum_name, GDExtensionConstStringNamePtr p_constant_name, GDExtensionInt p_constant_value, GDExtensionBool p_is_bitfield); @@ -96,8 +96,6 @@ class GDExtension : public Resource { int32_t level_initialized = -1; #ifdef TOOLS_ENABLED - uint64_t resource_last_modified_time = 0; - uint64_t library_last_modified_time = 0; bool is_reloading = false; Vector<GDExtensionMethodBind *> invalid_methods; Vector<ObjectID> instance_bindings; @@ -124,11 +122,12 @@ public: virtual bool editor_can_reload_from_file() override { return false; } // Reloading is handled in a special way. static String get_extension_list_config_file(); - static String find_extension_library(const String &p_path, Ref<ConfigFile> p_config, std::function<bool(String)> p_has_feature, PackedStringArray *r_tags = nullptr); - static Vector<SharedObject> find_extension_dependencies(const String &p_path, Ref<ConfigFile> p_config, std::function<bool(String)> p_has_feature); - Error open_library(const String &p_path, const String &p_entry_symbol, Vector<SharedObject> *p_dependencies = nullptr); + const Ref<GDExtensionLoader> get_loader() const { return loader; } + + Error open_library(const String &p_path, const Ref<GDExtensionLoader> &p_loader); void close_library(); + bool is_library_open() const; enum InitializationLevel { INITIALIZATION_LEVEL_CORE = GDEXTENSION_INITIALIZATION_CORE, @@ -146,17 +145,11 @@ protected: #endif public: - bool is_library_open() const; - #ifdef TOOLS_ENABLED bool is_reloadable() const { return reloadable; } void set_reloadable(bool p_reloadable) { reloadable = p_reloadable; } bool has_library_changed() const; - void update_last_modified_time(uint64_t p_resource_last_modified_time, uint64_t p_library_last_modified_time) { - resource_last_modified_time = p_resource_last_modified_time; - library_last_modified_time = p_library_last_modified_time; - } void track_instance_binding(Object *p_object); void untrack_instance_binding(Object *p_object); diff --git a/core/extension/gdextension_interface.cpp b/core/extension/gdextension_interface.cpp index 85f83eecfd..0ebe86d0a7 100644 --- a/core/extension/gdextension_interface.cpp +++ b/core/extension/gdextension_interface.cpp @@ -1299,7 +1299,7 @@ static void gdextension_object_call_script_method(GDExtensionObjectPtr p_object, const StringName method = *reinterpret_cast<const StringName *>(p_method); const Variant **args = (const Variant **)p_args; - Callable::CallError error; + Callable::CallError error; // TODO: Check `error`? memnew_placement(r_return, Variant); *(Variant *)r_return = o->callp(method, args, p_argument_count, error); @@ -1515,10 +1515,17 @@ static GDExtensionMethodBindPtr gdextension_classdb_get_method_bind(GDExtensionC return (GDExtensionMethodBindPtr)mb; } +#ifndef DISABLE_DEPRECATED static GDExtensionObjectPtr gdextension_classdb_construct_object(GDExtensionConstStringNamePtr p_classname) { const StringName classname = *reinterpret_cast<const StringName *>(p_classname); return (GDExtensionObjectPtr)ClassDB::instantiate_no_placeholders(classname); } +#endif + +static GDExtensionObjectPtr gdextension_classdb_construct_object2(GDExtensionConstStringNamePtr p_classname) { + const StringName classname = *reinterpret_cast<const StringName *>(p_classname); + return (GDExtensionObjectPtr)ClassDB::instantiate_without_postinitialization(classname); +} static void *gdextension_classdb_get_class_tag(GDExtensionConstStringNamePtr p_classname) { const StringName classname = *reinterpret_cast<const StringName *>(p_classname); @@ -1701,7 +1708,10 @@ void gdextension_setup_interface() { #endif // DISABLE_DEPRECATED REGISTER_INTERFACE_FUNC(callable_custom_create2); REGISTER_INTERFACE_FUNC(callable_custom_get_userdata); +#ifndef DISABLE_DEPRECATED REGISTER_INTERFACE_FUNC(classdb_construct_object); +#endif // DISABLE_DEPRECATED + REGISTER_INTERFACE_FUNC(classdb_construct_object2); REGISTER_INTERFACE_FUNC(classdb_get_method_bind); REGISTER_INTERFACE_FUNC(classdb_get_class_tag); REGISTER_INTERFACE_FUNC(editor_add_plugin); diff --git a/core/extension/gdextension_interface.h b/core/extension/gdextension_interface.h index fce377f967..9057e04bf3 100644 --- a/core/extension/gdextension_interface.h +++ b/core/extension/gdextension_interface.h @@ -268,6 +268,7 @@ typedef void (*GDExtensionClassReference)(GDExtensionClassInstancePtr p_instance typedef void (*GDExtensionClassUnreference)(GDExtensionClassInstancePtr p_instance); typedef void (*GDExtensionClassCallVirtual)(GDExtensionClassInstancePtr p_instance, const GDExtensionConstTypePtr *p_args, GDExtensionTypePtr r_ret); typedef GDExtensionObjectPtr (*GDExtensionClassCreateInstance)(void *p_class_userdata); +typedef GDExtensionObjectPtr (*GDExtensionClassCreateInstance2)(void *p_class_userdata, GDExtensionBool p_notify_postinitialize); typedef void (*GDExtensionClassFreeInstance)(void *p_class_userdata, GDExtensionClassInstancePtr p_instance); typedef GDExtensionClassInstancePtr (*GDExtensionClassRecreateInstance)(void *p_class_userdata, GDExtensionObjectPtr p_object); typedef GDExtensionClassCallVirtual (*GDExtensionClassGetVirtual)(void *p_class_userdata, GDExtensionConstStringNamePtr p_name); @@ -292,7 +293,7 @@ typedef struct { GDExtensionClassGetVirtual get_virtual_func; // Queries a virtual function by name and returns a callback to invoke the requested virtual function. GDExtensionClassGetRID get_rid_func; void *class_userdata; // Per-class user data, later accessible in instance bindings. -} GDExtensionClassCreationInfo; // Deprecated. Use GDExtensionClassCreationInfo3 instead. +} GDExtensionClassCreationInfo; // Deprecated. Use GDExtensionClassCreationInfo4 instead. typedef struct { GDExtensionBool is_virtual; @@ -325,7 +326,7 @@ typedef struct { GDExtensionClassCallVirtualWithData call_virtual_with_data_func; GDExtensionClassGetRID get_rid_func; void *class_userdata; // Per-class user data, later accessible in instance bindings. -} GDExtensionClassCreationInfo2; // Deprecated. Use GDExtensionClassCreationInfo3 instead. +} GDExtensionClassCreationInfo2; // Deprecated. Use GDExtensionClassCreationInfo4 instead. typedef struct { GDExtensionBool is_virtual; @@ -359,7 +360,41 @@ typedef struct { GDExtensionClassCallVirtualWithData call_virtual_with_data_func; GDExtensionClassGetRID get_rid_func; void *class_userdata; // Per-class user data, later accessible in instance bindings. -} GDExtensionClassCreationInfo3; +} GDExtensionClassCreationInfo3; // Deprecated. Use GDExtensionClassCreationInfo4 instead. + +typedef struct { + GDExtensionBool is_virtual; + GDExtensionBool is_abstract; + GDExtensionBool is_exposed; + GDExtensionBool is_runtime; + GDExtensionClassSet set_func; + GDExtensionClassGet get_func; + GDExtensionClassGetPropertyList get_property_list_func; + GDExtensionClassFreePropertyList2 free_property_list_func; + GDExtensionClassPropertyCanRevert property_can_revert_func; + GDExtensionClassPropertyGetRevert property_get_revert_func; + GDExtensionClassValidateProperty validate_property_func; + GDExtensionClassNotification2 notification_func; + GDExtensionClassToString to_string_func; + GDExtensionClassReference reference_func; + GDExtensionClassUnreference unreference_func; + GDExtensionClassCreateInstance2 create_instance_func; // (Default) constructor; mandatory. If the class is not instantiable, consider making it virtual or abstract. + GDExtensionClassFreeInstance free_instance_func; // Destructor; mandatory. + GDExtensionClassRecreateInstance recreate_instance_func; + // Queries a virtual function by name and returns a callback to invoke the requested virtual function. + GDExtensionClassGetVirtual get_virtual_func; + // Paired with `call_virtual_with_data_func`, this is an alternative to `get_virtual_func` for extensions that + // need or benefit from extra data when calling virtual functions. + // Returns user data that will be passed to `call_virtual_with_data_func`. + // Returning `NULL` from this function signals to Godot that the virtual function is not overridden. + // Data returned from this function should be managed by the extension and must be valid until the extension is deinitialized. + // You should supply either `get_virtual_func`, or `get_virtual_call_data_func` with `call_virtual_with_data_func`. + GDExtensionClassGetVirtualCallData get_virtual_call_data_func; + // Used to call virtual functions when `get_virtual_call_data_func` is not null. + GDExtensionClassCallVirtualWithData call_virtual_with_data_func; + GDExtensionClassGetRID get_rid_func; + void *class_userdata; // Per-class user data, later accessible in instance bindings. +} GDExtensionClassCreationInfo4; typedef void *GDExtensionClassLibraryPtr; @@ -2680,6 +2715,7 @@ typedef void *(*GDExtensionInterfaceCallableCustomGetUserData)(GDExtensionConstT /** * @name classdb_construct_object * @since 4.1 + * @deprecated in Godot 4.4. Use `classdb_construct_object2` instead. * * Constructs an Object of the requested class. * @@ -2692,6 +2728,22 @@ typedef void *(*GDExtensionInterfaceCallableCustomGetUserData)(GDExtensionConstT typedef GDExtensionObjectPtr (*GDExtensionInterfaceClassdbConstructObject)(GDExtensionConstStringNamePtr p_classname); /** + * @name classdb_construct_object2 + * @since 4.4 + * + * Constructs an Object of the requested class. + * + * The passed class must be a built-in godot class, or an already-registered extension class. In both cases, object_set_instance() should be called to fully initialize the object. + * + * "NOTIFICATION_POSTINITIALIZE" must be sent after construction. + * + * @param p_classname A pointer to a StringName with the class name. + * + * @return A pointer to the newly created Object. + */ +typedef GDExtensionObjectPtr (*GDExtensionInterfaceClassdbConstructObject2)(GDExtensionConstStringNamePtr p_classname); + +/** * @name classdb_get_method_bind * @since 4.1 * @@ -2722,7 +2774,7 @@ typedef void *(*GDExtensionInterfaceClassdbGetClassTag)(GDExtensionConstStringNa /** * @name classdb_register_extension_class * @since 4.1 - * @deprecated in Godot 4.2. Use `classdb_register_extension_class3` instead. + * @deprecated in Godot 4.2. Use `classdb_register_extension_class4` instead. * * Registers an extension class in the ClassDB. * @@ -2738,7 +2790,7 @@ typedef void (*GDExtensionInterfaceClassdbRegisterExtensionClass)(GDExtensionCla /** * @name classdb_register_extension_class2 * @since 4.2 - * @deprecated in Godot 4.3. Use `classdb_register_extension_class3` instead. + * @deprecated in Godot 4.3. Use `classdb_register_extension_class4` instead. * * Registers an extension class in the ClassDB. * @@ -2754,6 +2806,7 @@ typedef void (*GDExtensionInterfaceClassdbRegisterExtensionClass2)(GDExtensionCl /** * @name classdb_register_extension_class3 * @since 4.3 + * @deprecated in Godot 4.4. Use `classdb_register_extension_class4` instead. * * Registers an extension class in the ClassDB. * @@ -2767,6 +2820,21 @@ typedef void (*GDExtensionInterfaceClassdbRegisterExtensionClass2)(GDExtensionCl typedef void (*GDExtensionInterfaceClassdbRegisterExtensionClass3)(GDExtensionClassLibraryPtr p_library, GDExtensionConstStringNamePtr p_class_name, GDExtensionConstStringNamePtr p_parent_class_name, const GDExtensionClassCreationInfo3 *p_extension_funcs); /** + * @name classdb_register_extension_class4 + * @since 4.4 + * + * Registers an extension class in the ClassDB. + * + * Provided struct can be safely freed once the function returns. + * + * @param p_library A pointer the library received by the GDExtension's entry point function. + * @param p_class_name A pointer to a StringName with the class name. + * @param p_parent_class_name A pointer to a StringName with the parent class name. + * @param p_extension_funcs A pointer to a GDExtensionClassCreationInfo2 struct. + */ +typedef void (*GDExtensionInterfaceClassdbRegisterExtensionClass4)(GDExtensionClassLibraryPtr p_library, GDExtensionConstStringNamePtr p_class_name, GDExtensionConstStringNamePtr p_parent_class_name, const GDExtensionClassCreationInfo4 *p_extension_funcs); + +/** * @name classdb_register_extension_class_method * @since 4.1 * diff --git a/core/extension/gdextension_library_loader.cpp b/core/extension/gdextension_library_loader.cpp new file mode 100644 index 0000000000..5ba4933c35 --- /dev/null +++ b/core/extension/gdextension_library_loader.cpp @@ -0,0 +1,390 @@ +/**************************************************************************/ +/* gdextension_library_loader.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 "gdextension_library_loader.h" + +#include "core/config/project_settings.h" +#include "core/io/dir_access.h" +#include "core/version.h" +#include "gdextension.h" + +Vector<SharedObject> GDExtensionLibraryLoader::find_extension_dependencies(const String &p_path, Ref<ConfigFile> p_config, std::function<bool(String)> p_has_feature) { + Vector<SharedObject> dependencies_shared_objects; + if (p_config->has_section("dependencies")) { + List<String> config_dependencies; + p_config->get_section_keys("dependencies", &config_dependencies); + + for (const String &dependency : config_dependencies) { + Vector<String> dependency_tags = dependency.split("."); + bool all_tags_met = true; + for (int i = 0; i < dependency_tags.size(); i++) { + String tag = dependency_tags[i].strip_edges(); + if (!p_has_feature(tag)) { + all_tags_met = false; + break; + } + } + + if (all_tags_met) { + Dictionary dependency_value = p_config->get_value("dependencies", dependency); + for (const Variant *key = dependency_value.next(nullptr); key; key = dependency_value.next(key)) { + String dependency_path = *key; + String target_path = dependency_value[*key]; + if (dependency_path.is_relative_path()) { + dependency_path = p_path.get_base_dir().path_join(dependency_path); + } + dependencies_shared_objects.push_back(SharedObject(dependency_path, dependency_tags, target_path)); + } + break; + } + } + } + + return dependencies_shared_objects; +} + +String GDExtensionLibraryLoader::find_extension_library(const String &p_path, Ref<ConfigFile> p_config, std::function<bool(String)> p_has_feature, PackedStringArray *r_tags) { + // First, check the explicit libraries. + if (p_config->has_section("libraries")) { + List<String> libraries; + p_config->get_section_keys("libraries", &libraries); + + // Iterate the libraries, finding the best matching tags. + String best_library_path; + Vector<String> best_library_tags; + for (const String &E : libraries) { + Vector<String> tags = E.split("."); + bool all_tags_met = true; + for (int i = 0; i < tags.size(); i++) { + String tag = tags[i].strip_edges(); + if (!p_has_feature(tag)) { + all_tags_met = false; + break; + } + } + + if (all_tags_met && tags.size() > best_library_tags.size()) { + best_library_path = p_config->get_value("libraries", E); + best_library_tags = tags; + } + } + + if (!best_library_path.is_empty()) { + if (best_library_path.is_relative_path()) { + best_library_path = p_path.get_base_dir().path_join(best_library_path); + } + if (r_tags != nullptr) { + r_tags->append_array(best_library_tags); + } + return best_library_path; + } + } + + // Second, try to autodetect. + String autodetect_library_prefix; + if (p_config->has_section_key("configuration", "autodetect_library_prefix")) { + autodetect_library_prefix = p_config->get_value("configuration", "autodetect_library_prefix"); + } + if (!autodetect_library_prefix.is_empty()) { + String autodetect_path = autodetect_library_prefix; + if (autodetect_path.is_relative_path()) { + autodetect_path = p_path.get_base_dir().path_join(autodetect_path); + } + + // Find the folder and file parts of the prefix. + String folder; + String file_prefix; + if (DirAccess::dir_exists_absolute(autodetect_path)) { + folder = autodetect_path; + } else if (DirAccess::dir_exists_absolute(autodetect_path.get_base_dir())) { + folder = autodetect_path.get_base_dir(); + file_prefix = autodetect_path.get_file(); + } else { + ERR_FAIL_V_MSG(String(), vformat("Error in extension: %s. Could not find folder for automatic detection of libraries files. autodetect_library_prefix=\"%s\"", p_path, autodetect_library_prefix)); + } + + // Open the folder. + Ref<DirAccess> dir = DirAccess::open(folder); + ERR_FAIL_COND_V_MSG(dir.is_null(), String(), vformat("Error in extension: %s. Could not open folder for automatic detection of libraries files. autodetect_library_prefix=\"%s\"", p_path, autodetect_library_prefix)); + + // Iterate the files and check the prefixes, finding the best matching file. + String best_file; + Vector<String> best_file_tags; + dir->list_dir_begin(); + String file_name = dir->_get_next(); + while (file_name != "") { + if (!dir->current_is_dir() && file_name.begins_with(file_prefix)) { + // Check if the files matches all requested feature tags. + String tags_str = file_name.trim_prefix(file_prefix); + tags_str = tags_str.trim_suffix(tags_str.get_extension()); + + Vector<String> tags = tags_str.split(".", false); + bool all_tags_met = true; + for (int i = 0; i < tags.size(); i++) { + String tag = tags[i].strip_edges(); + if (!p_has_feature(tag)) { + all_tags_met = false; + break; + } + } + + // If all tags are found in the feature list, and we found more tags than before, use this file. + if (all_tags_met && tags.size() > best_file_tags.size()) { + best_file_tags = tags; + best_file = file_name; + } + } + file_name = dir->_get_next(); + } + + if (!best_file.is_empty()) { + String library_path = folder.path_join(best_file); + if (r_tags != nullptr) { + r_tags->append_array(best_file_tags); + } + return library_path; + } + } + return String(); +} + +Error GDExtensionLibraryLoader::open_library(const String &p_path) { + Error err = parse_gdextension_file(p_path); + if (err != OK) { + return err; + } + + String abs_path = ProjectSettings::get_singleton()->globalize_path(library_path); + + Vector<String> abs_dependencies_paths; + if (!library_dependencies.is_empty()) { + for (const SharedObject &dependency : library_dependencies) { + abs_dependencies_paths.push_back(ProjectSettings::get_singleton()->globalize_path(dependency.path)); + } + } + + OS::GDExtensionData data = { + true, // also_set_library_path + &library_path, // r_resolved_path + Engine::get_singleton()->is_editor_hint(), // generate_temp_files + &abs_dependencies_paths, // library_dependencies + }; + + err = OS::get_singleton()->open_dynamic_library(is_static_library ? String() : abs_path, library, &data); + if (err != OK) { + return err; + } + + return OK; +} + +Error GDExtensionLibraryLoader::initialize(GDExtensionInterfaceGetProcAddress p_get_proc_address, const Ref<GDExtension> &p_extension, GDExtensionInitialization *r_initialization) { +#ifdef TOOLS_ENABLED + p_extension->set_reloadable(is_reloadable && Engine::get_singleton()->is_extension_reloading_enabled()); +#endif + + for (const KeyValue<String, String> &icon : class_icon_paths) { + p_extension->class_icon_paths[icon.key] = icon.value; + } + + void *entry_funcptr = nullptr; + + Error err = OS::get_singleton()->get_dynamic_library_symbol_handle(library, entry_symbol, entry_funcptr, false); + + if (err != OK) { + ERR_PRINT("GDExtension entry point '" + entry_symbol + "' not found in library " + library_path); + return err; + } + + GDExtensionInitializationFunction initialization_function = (GDExtensionInitializationFunction)entry_funcptr; + + GDExtensionBool ret = initialization_function(p_get_proc_address, p_extension.ptr(), r_initialization); + + if (ret) { + return OK; + } else { + ERR_PRINT("GDExtension initialization function '" + entry_symbol + "' returned an error."); + return FAILED; + } +} + +void GDExtensionLibraryLoader::close_library() { + OS::get_singleton()->close_dynamic_library(library); + library = nullptr; +} + +bool GDExtensionLibraryLoader::is_library_open() const { + return library != nullptr; +} + +bool GDExtensionLibraryLoader::has_library_changed() const { +#ifdef TOOLS_ENABLED + // Check only that the last modified time is different (rather than checking + // that it's newer) since some OS's (namely Windows) will preserve the modified + // time by default when copying files. + if (FileAccess::get_modified_time(resource_path) != resource_last_modified_time) { + return true; + } + if (FileAccess::get_modified_time(library_path) != library_last_modified_time) { + return true; + } +#endif + return false; +} + +Error GDExtensionLibraryLoader::parse_gdextension_file(const String &p_path) { + resource_path = p_path; + + Ref<ConfigFile> config; + config.instantiate(); + + Error err = config->load(p_path); + + if (err != OK) { + ERR_PRINT("Error loading GDExtension configuration file: " + p_path); + return err; + } + + if (!config->has_section_key("configuration", "entry_symbol")) { + ERR_PRINT("GDExtension configuration file must contain a \"configuration/entry_symbol\" key: " + p_path); + return ERR_INVALID_DATA; + } + + entry_symbol = config->get_value("configuration", "entry_symbol"); + + uint32_t compatibility_minimum[3] = { 0, 0, 0 }; + if (config->has_section_key("configuration", "compatibility_minimum")) { + String compat_string = config->get_value("configuration", "compatibility_minimum"); + Vector<int> parts = compat_string.split_ints("."); + for (int i = 0; i < parts.size(); i++) { + if (i >= 3) { + break; + } + if (parts[i] >= 0) { + compatibility_minimum[i] = parts[i]; + } + } + } else { + ERR_PRINT("GDExtension configuration file must contain a \"configuration/compatibility_minimum\" key: " + p_path); + return ERR_INVALID_DATA; + } + + if (compatibility_minimum[0] < 4 || (compatibility_minimum[0] == 4 && compatibility_minimum[1] == 0)) { + ERR_PRINT(vformat("GDExtension's compatibility_minimum (%d.%d.%d) must be at least 4.1.0: %s", compatibility_minimum[0], compatibility_minimum[1], compatibility_minimum[2], p_path)); + return ERR_INVALID_DATA; + } + + bool compatible = true; + // Check version lexicographically. + if (VERSION_MAJOR != compatibility_minimum[0]) { + compatible = VERSION_MAJOR > compatibility_minimum[0]; + } else if (VERSION_MINOR != compatibility_minimum[1]) { + compatible = VERSION_MINOR > compatibility_minimum[1]; + } else { + compatible = VERSION_PATCH >= compatibility_minimum[2]; + } + if (!compatible) { + ERR_PRINT(vformat("GDExtension only compatible with Godot version %d.%d.%d or later: %s", compatibility_minimum[0], compatibility_minimum[1], compatibility_minimum[2], p_path)); + return ERR_INVALID_DATA; + } + + // Optionally check maximum compatibility. + if (config->has_section_key("configuration", "compatibility_maximum")) { + uint32_t compatibility_maximum[3] = { 0, 0, 0 }; + String compat_string = config->get_value("configuration", "compatibility_maximum"); + Vector<int> parts = compat_string.split_ints("."); + for (int i = 0; i < 3; i++) { + if (i < parts.size() && parts[i] >= 0) { + compatibility_maximum[i] = parts[i]; + } else { + // If a version part is missing, set the maximum to an arbitrary high value. + compatibility_maximum[i] = 9999; + } + } + + compatible = true; + if (VERSION_MAJOR != compatibility_maximum[0]) { + compatible = VERSION_MAJOR < compatibility_maximum[0]; + } else if (VERSION_MINOR != compatibility_maximum[1]) { + compatible = VERSION_MINOR < compatibility_maximum[1]; + } +#if VERSION_PATCH + // #if check to avoid -Wtype-limits warning when 0. + else { + compatible = VERSION_PATCH <= compatibility_maximum[2]; + } +#endif + + if (!compatible) { + ERR_PRINT(vformat("GDExtension only compatible with Godot version %s or earlier: %s", compat_string, p_path)); + return ERR_INVALID_DATA; + } + } + + library_path = find_extension_library(p_path, config, [](const String &p_feature) { return OS::get_singleton()->has_feature(p_feature); }); + + if (library_path.is_empty()) { + const String os_arch = OS::get_singleton()->get_name().to_lower() + "." + Engine::get_singleton()->get_architecture_name(); + ERR_PRINT(vformat("No GDExtension library found for current OS and architecture (%s) in configuration file: %s", os_arch, p_path)); + return ERR_FILE_NOT_FOUND; + } + + is_static_library = library_path.ends_with(".a") || library_path.ends_with(".xcframework"); + + if (!library_path.is_resource_file() && !library_path.is_absolute_path()) { + library_path = p_path.get_base_dir().path_join(library_path); + } + +#ifdef TOOLS_ENABLED + is_reloadable = config->get_value("configuration", "reloadable", false); + + update_last_modified_time( + FileAccess::get_modified_time(resource_path), + FileAccess::get_modified_time(library_path)); +#endif + + library_dependencies = find_extension_dependencies(p_path, config, [](const String &p_feature) { return OS::get_singleton()->has_feature(p_feature); }); + + // Handle icons if any are specified. + if (config->has_section("icons")) { + List<String> keys; + config->get_section_keys("icons", &keys); + for (const String &key : keys) { + String icon_path = config->get_value("icons", key); + if (icon_path.is_relative_path()) { + icon_path = p_path.get_base_dir().path_join(icon_path); + } + + class_icon_paths[key] = icon_path; + } + } + + return OK; +} diff --git a/core/extension/gdextension_library_loader.h b/core/extension/gdextension_library_loader.h new file mode 100644 index 0000000000..f4372a75d4 --- /dev/null +++ b/core/extension/gdextension_library_loader.h @@ -0,0 +1,84 @@ +/**************************************************************************/ +/* gdextension_library_loader.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. */ +/**************************************************************************/ + +#ifndef GDEXTENSION_LIBRARY_LOADER_H +#define GDEXTENSION_LIBRARY_LOADER_H + +#include <functional> + +#include "core/extension/gdextension_loader.h" +#include "core/io/config_file.h" +#include "core/os/shared_object.h" + +class GDExtensionLibraryLoader : public GDExtensionLoader { + friend class GDExtensionManager; + friend class GDExtension; + +private: + String resource_path; + + void *library = nullptr; // pointer if valid. + String library_path; + String entry_symbol; + + bool is_static_library = false; + +#ifdef TOOLS_ENABLED + bool is_reloadable = false; +#endif + + Vector<SharedObject> library_dependencies; + + HashMap<String, String> class_icon_paths; + +#ifdef TOOLS_ENABLED + uint64_t resource_last_modified_time = 0; + uint64_t library_last_modified_time = 0; + + void update_last_modified_time(uint64_t p_resource_last_modified_time, uint64_t p_library_last_modified_time) { + resource_last_modified_time = p_resource_last_modified_time; + library_last_modified_time = p_library_last_modified_time; + } +#endif + +public: + static String find_extension_library(const String &p_path, Ref<ConfigFile> p_config, std::function<bool(String)> p_has_feature, PackedStringArray *r_tags = nullptr); + static Vector<SharedObject> find_extension_dependencies(const String &p_path, Ref<ConfigFile> p_config, std::function<bool(String)> p_has_feature); + + virtual Error open_library(const String &p_path) override; + virtual Error initialize(GDExtensionInterfaceGetProcAddress p_get_proc_address, const Ref<GDExtension> &p_extension, GDExtensionInitialization *r_initialization) override; + virtual void close_library() override; + virtual bool is_library_open() const override; + virtual bool has_library_changed() const override; + + Error parse_gdextension_file(const String &p_path); +}; + +#endif // GDEXTENSION_LIBRARY_LOADER_H diff --git a/core/extension/gdextension_loader.h b/core/extension/gdextension_loader.h new file mode 100644 index 0000000000..7d779858b7 --- /dev/null +++ b/core/extension/gdextension_loader.h @@ -0,0 +1,47 @@ +/**************************************************************************/ +/* gdextension_loader.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. */ +/**************************************************************************/ + +#ifndef GDEXTENSION_LOADER_H +#define GDEXTENSION_LOADER_H + +#include "core/object/ref_counted.h" + +class GDExtension; + +class GDExtensionLoader : public RefCounted { +public: + virtual Error open_library(const String &p_path) = 0; + virtual Error initialize(GDExtensionInterfaceGetProcAddress p_get_proc_address, const Ref<GDExtension> &p_extension, GDExtensionInitialization *r_initialization) = 0; + virtual void close_library() = 0; + virtual bool is_library_open() const = 0; + virtual bool has_library_changed() const = 0; +}; + +#endif // GDEXTENSION_LOADER_H diff --git a/core/extension/gdextension_manager.cpp b/core/extension/gdextension_manager.cpp index 1ee9de0776..01efe0d96e 100644 --- a/core/extension/gdextension_manager.cpp +++ b/core/extension/gdextension_manager.cpp @@ -31,14 +31,19 @@ #include "gdextension_manager.h" #include "core/extension/gdextension_compat_hashes.h" +#include "core/extension/gdextension_library_loader.h" +#include "core/io/dir_access.h" #include "core/io/file_access.h" #include "core/object/script_language.h" -GDExtensionManager::LoadStatus GDExtensionManager::_load_extension_internal(const Ref<GDExtension> &p_extension) { +GDExtensionManager::LoadStatus GDExtensionManager::_load_extension_internal(const Ref<GDExtension> &p_extension, bool p_first_load) { if (level >= 0) { // Already initialized up to some level. - int32_t minimum_level = p_extension->get_minimum_library_initialization_level(); - if (minimum_level < MIN(level, GDExtension::INITIALIZATION_LEVEL_SCENE)) { - return LOAD_STATUS_NEEDS_RESTART; + int32_t minimum_level = 0; + if (!p_first_load) { + minimum_level = p_extension->get_minimum_library_initialization_level(); + if (minimum_level < MIN(level, GDExtension::INITIALIZATION_LEVEL_SCENE)) { + return LOAD_STATUS_NEEDS_RESTART; + } } // Initialize up to current level. for (int32_t i = minimum_level; i <= level; i++) { @@ -50,10 +55,20 @@ GDExtensionManager::LoadStatus GDExtensionManager::_load_extension_internal(cons gdextension_class_icon_paths[kv.key] = kv.value; } +#ifdef TOOLS_ENABLED + // Signals that a new extension is loaded so GDScript can register new class names. + emit_signal("extension_loaded", p_extension); +#endif + return LOAD_STATUS_OK; } GDExtensionManager::LoadStatus GDExtensionManager::_unload_extension_internal(const Ref<GDExtension> &p_extension) { +#ifdef TOOLS_ENABLED + // Signals that a new extension is unloading so GDScript can unregister class names. + emit_signal("extension_unloading", p_extension); +#endif + if (level >= 0) { // Already initialized up to some level. // Deinitialize down from current level. for (int32_t i = level; i >= GDExtension::INITIALIZATION_LEVEL_CORE; i--) { @@ -69,19 +84,31 @@ GDExtensionManager::LoadStatus GDExtensionManager::_unload_extension_internal(co } GDExtensionManager::LoadStatus GDExtensionManager::load_extension(const String &p_path) { + Ref<GDExtensionLibraryLoader> loader; + loader.instantiate(); + return GDExtensionManager::get_singleton()->load_extension_with_loader(p_path, loader); +} + +GDExtensionManager::LoadStatus GDExtensionManager::load_extension_with_loader(const String &p_path, const Ref<GDExtensionLoader> &p_loader) { + DEV_ASSERT(p_loader.is_valid()); + if (gdextension_map.has(p_path)) { return LOAD_STATUS_ALREADY_LOADED; } - Ref<GDExtension> extension = ResourceLoader::load(p_path); - if (extension.is_null()) { + + Ref<GDExtension> extension; + extension.instantiate(); + Error err = extension->open_library(p_path, p_loader); + if (err != OK) { return LOAD_STATUS_FAILED; } - LoadStatus status = _load_extension_internal(extension); + LoadStatus status = _load_extension_internal(extension, true); if (status != LOAD_STATUS_OK) { return status; } + extension->set_path(p_path); gdextension_map[p_path] = extension; return LOAD_STATUS_OK; } @@ -117,12 +144,12 @@ GDExtensionManager::LoadStatus GDExtensionManager::reload_extension(const String extension->close_library(); } - Error err = GDExtensionResourceLoader::load_gdextension_resource(p_path, extension); + Error err = extension->open_library(p_path, extension->loader); if (err != OK) { return LOAD_STATUS_FAILED; } - status = _load_extension_internal(extension); + status = _load_extension_internal(extension, false); if (status != LOAD_STATUS_OK) { return status; } @@ -261,6 +288,71 @@ void GDExtensionManager::reload_extensions() { #endif } +bool GDExtensionManager::ensure_extensions_loaded(const HashSet<String> &p_extensions) { + Vector<String> extensions_added; + Vector<String> extensions_removed; + + for (const String &E : p_extensions) { + if (!is_extension_loaded(E)) { + extensions_added.push_back(E); + } + } + + Vector<String> loaded_extensions = get_loaded_extensions(); + for (const String &loaded_extension : loaded_extensions) { + if (!p_extensions.has(loaded_extension)) { + // The extension may not have a .gdextension file. + if (!FileAccess::exists(loaded_extension)) { + extensions_removed.push_back(loaded_extension); + } + } + } + + String extension_list_config_file = GDExtension::get_extension_list_config_file(); + if (p_extensions.size()) { + if (extensions_added.size() || extensions_removed.size()) { + // Extensions were added or removed. + Ref<FileAccess> f = FileAccess::open(extension_list_config_file, FileAccess::WRITE); + for (const String &E : p_extensions) { + f->store_line(E); + } + } + } else { + if (loaded_extensions.size() || FileAccess::exists(extension_list_config_file)) { + // Extensions were removed. + Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_RESOURCES); + da->remove(extension_list_config_file); + } + } + + bool needs_restart = false; + for (const String &extension : extensions_added) { + GDExtensionManager::LoadStatus st = GDExtensionManager::get_singleton()->load_extension(extension); + if (st == GDExtensionManager::LOAD_STATUS_NEEDS_RESTART) { + needs_restart = true; + } + } + + for (const String &extension : extensions_removed) { + GDExtensionManager::LoadStatus st = GDExtensionManager::get_singleton()->unload_extension(extension); + if (st == GDExtensionManager::LOAD_STATUS_NEEDS_RESTART) { + needs_restart = true; + } + } + +#ifdef TOOLS_ENABLED + if (extensions_added.size() || extensions_removed.size()) { + // Emitting extensions_reloaded so EditorNode can reload Inspector and regenerate documentation. + emit_signal("extensions_reloaded"); + + // Reload all scripts to clear out old references. + callable_mp_static(&GDExtensionManager::_reload_all_scripts).call_deferred(); + } +#endif + + return needs_restart; +} + GDExtensionManager *GDExtensionManager::get_singleton() { return singleton; } @@ -281,6 +373,8 @@ void GDExtensionManager::_bind_methods() { BIND_ENUM_CONSTANT(LOAD_STATUS_NEEDS_RESTART); ADD_SIGNAL(MethodInfo("extensions_reloaded")); + ADD_SIGNAL(MethodInfo("extension_loaded", PropertyInfo(Variant::OBJECT, "extension", PROPERTY_HINT_RESOURCE_TYPE, "GDExtension"))); + ADD_SIGNAL(MethodInfo("extension_unloading", PropertyInfo(Variant::OBJECT, "extension", PROPERTY_HINT_RESOURCE_TYPE, "GDExtension"))); } GDExtensionManager *GDExtensionManager::singleton = nullptr; diff --git a/core/extension/gdextension_manager.h b/core/extension/gdextension_manager.h index 9386e356bb..39a600474c 100644 --- a/core/extension/gdextension_manager.h +++ b/core/extension/gdextension_manager.h @@ -54,7 +54,7 @@ public: }; private: - LoadStatus _load_extension_internal(const Ref<GDExtension> &p_extension); + LoadStatus _load_extension_internal(const Ref<GDExtension> &p_extension, bool p_first_load); LoadStatus _unload_extension_internal(const Ref<GDExtension> &p_extension); #ifdef TOOLS_ENABLED @@ -63,6 +63,7 @@ private: public: LoadStatus load_extension(const String &p_path); + LoadStatus load_extension_with_loader(const String &p_path, const Ref<GDExtensionLoader> &p_loader); LoadStatus reload_extension(const String &p_path); LoadStatus unload_extension(const String &p_path); bool is_extension_loaded(const String &p_path) const; @@ -84,6 +85,7 @@ public: void load_extensions(); void reload_extensions(); + bool ensure_extensions_loaded(const HashSet<String> &p_extensions); GDExtensionManager(); ~GDExtensionManager(); diff --git a/core/io/dtls_server.cpp b/core/io/dtls_server.cpp index 07d62d3a8d..7638328dc3 100644 --- a/core/io/dtls_server.cpp +++ b/core/io/dtls_server.cpp @@ -33,12 +33,12 @@ #include "core/config/project_settings.h" #include "core/io/file_access.h" -DTLSServer *(*DTLSServer::_create)() = nullptr; +DTLSServer *(*DTLSServer::_create)(bool p_notify_postinitialize) = nullptr; bool DTLSServer::available = false; -DTLSServer *DTLSServer::create() { +DTLSServer *DTLSServer::create(bool p_notify_postinitialize) { if (_create) { - return _create(); + return _create(p_notify_postinitialize); } return nullptr; } diff --git a/core/io/dtls_server.h b/core/io/dtls_server.h index f3fbde3c15..5ffed1ecc3 100644 --- a/core/io/dtls_server.h +++ b/core/io/dtls_server.h @@ -38,14 +38,14 @@ class DTLSServer : public RefCounted { GDCLASS(DTLSServer, RefCounted); protected: - static DTLSServer *(*_create)(); + static DTLSServer *(*_create)(bool p_notify_postinitialize); static void _bind_methods(); static bool available; public: static bool is_available(); - static DTLSServer *create(); + static DTLSServer *create(bool p_notify_postinitialize = true); virtual Error setup(Ref<TLSOptions> p_options) = 0; virtual void stop() = 0; diff --git a/core/io/file_access.cpp b/core/io/file_access.cpp index 1cf388b33a..d919243e6b 100644 --- a/core/io/file_access.cpp +++ b/core/io/file_access.cpp @@ -59,11 +59,9 @@ bool FileAccess::exists(const String &p_name) { return true; } - Ref<FileAccess> f = open(p_name, READ); - if (f.is_null()) { - return false; - } - return true; + // Using file_exists because it's faster than trying to open the file. + Ref<FileAccess> ret = create_for_path(p_name); + return ret->file_exists(p_name); } void FileAccess::_set_access_type(AccessType p_access) { @@ -225,59 +223,44 @@ String FileAccess::fix_path(const String &p_path) const { } /* these are all implemented for ease of porting, then can later be optimized */ +uint8_t FileAccess::get_8() const { + uint8_t data = 0; + get_buffer(&data, sizeof(uint8_t)); -uint16_t FileAccess::get_16() const { - uint16_t res; - uint8_t a, b; + return data; +} - a = get_8(); - b = get_8(); +uint16_t FileAccess::get_16() const { + uint16_t data = 0; + get_buffer(reinterpret_cast<uint8_t *>(&data), sizeof(uint16_t)); if (big_endian) { - SWAP(a, b); + data = BSWAP16(data); } - res = b; - res <<= 8; - res |= a; - - return res; + return data; } uint32_t FileAccess::get_32() const { - uint32_t res; - uint16_t a, b; - - a = get_16(); - b = get_16(); + uint32_t data = 0; + get_buffer(reinterpret_cast<uint8_t *>(&data), sizeof(uint32_t)); if (big_endian) { - SWAP(a, b); + data = BSWAP32(data); } - res = b; - res <<= 16; - res |= a; - - return res; + return data; } uint64_t FileAccess::get_64() const { - uint64_t res; - uint32_t a, b; - - a = get_32(); - b = get_32(); + uint64_t data = 0; + get_buffer(reinterpret_cast<uint8_t *>(&data), sizeof(uint64_t)); if (big_endian) { - SWAP(a, b); + data = BSWAP64(data); } - res = b; - res <<= 32; - res |= a; - - return res; + return data; } float FileAccess::get_float() const { @@ -467,17 +450,6 @@ String FileAccess::get_as_text(bool p_skip_cr) const { return text; } -uint64_t FileAccess::get_buffer(uint8_t *p_dst, uint64_t p_length) const { - ERR_FAIL_COND_V(!p_dst && p_length > 0, -1); - - uint64_t i = 0; - for (i = 0; i < p_length && !eof_reached(); i++) { - p_dst[i] = get_8(); - } - - return i; -} - Vector<uint8_t> FileAccess::get_buffer(int64_t p_length) const { Vector<uint8_t> data; @@ -490,7 +462,7 @@ Vector<uint8_t> FileAccess::get_buffer(int64_t p_length) const { ERR_FAIL_COND_V_MSG(err != OK, data, "Can't resize data to " + itos(p_length) + " elements."); uint8_t *w = data.ptrw(); - int64_t len = get_buffer(&w[0], p_length); + int64_t len = get_buffer(w, p_length); if (len < p_length) { data.resize(len); @@ -514,46 +486,32 @@ String FileAccess::get_as_utf8_string(bool p_skip_cr) const { return s; } -void FileAccess::store_16(uint16_t p_dest) { - uint8_t a, b; - - a = p_dest & 0xFF; - b = p_dest >> 8; +void FileAccess::store_8(uint8_t p_dest) { + store_buffer(&p_dest, sizeof(uint8_t)); +} +void FileAccess::store_16(uint16_t p_dest) { if (big_endian) { - SWAP(a, b); + p_dest = BSWAP16(p_dest); } - store_8(a); - store_8(b); + store_buffer(reinterpret_cast<uint8_t *>(&p_dest), sizeof(uint16_t)); } void FileAccess::store_32(uint32_t p_dest) { - uint16_t a, b; - - a = p_dest & 0xFFFF; - b = p_dest >> 16; - if (big_endian) { - SWAP(a, b); + p_dest = BSWAP32(p_dest); } - store_16(a); - store_16(b); + store_buffer(reinterpret_cast<uint8_t *>(&p_dest), sizeof(uint32_t)); } void FileAccess::store_64(uint64_t p_dest) { - uint32_t a, b; - - a = p_dest & 0xFFFFFFFF; - b = p_dest >> 32; - if (big_endian) { - SWAP(a, b); + p_dest = BSWAP64(p_dest); } - store_32(a); - store_32(b); + store_buffer(reinterpret_cast<uint8_t *>(&p_dest), sizeof(uint64_t)); } void FileAccess::store_real(real_t p_real) { @@ -710,22 +668,11 @@ void FileAccess::store_csv_line(const Vector<String> &p_values, const String &p_ store_line(line); } -void FileAccess::store_buffer(const uint8_t *p_src, uint64_t p_length) { - ERR_FAIL_COND(!p_src && p_length > 0); - for (uint64_t i = 0; i < p_length; i++) { - store_8(p_src[i]); - } -} - void FileAccess::store_buffer(const Vector<uint8_t> &p_buffer) { uint64_t len = p_buffer.size(); - if (len == 0) { - return; - } - const uint8_t *r = p_buffer.ptr(); - store_buffer(&r[0], len); + store_buffer(r, len); } void FileAccess::store_var(const Variant &p_var, bool p_full_objects) { diff --git a/core/io/file_access.h b/core/io/file_access.h index 2ab84db4b6..2f4d1a8604 100644 --- a/core/io/file_access.h +++ b/core/io/file_access.h @@ -137,7 +137,7 @@ public: virtual bool eof_reached() const = 0; ///< reading passed EOF - virtual uint8_t get_8() const = 0; ///< get a byte + virtual uint8_t get_8() const; ///< get a byte virtual uint16_t get_16() const; ///< get 16 bits uint virtual uint32_t get_32() const; ///< get 32 bits uint virtual uint64_t get_64() const; ///< get 64 bits uint @@ -148,7 +148,7 @@ public: Variant get_var(bool p_allow_objects = false) const; - virtual uint64_t get_buffer(uint8_t *p_dst, uint64_t p_length) const; ///< get an array of bytes + virtual uint64_t get_buffer(uint8_t *p_dst, uint64_t p_length) const = 0; ///< get an array of bytes, needs to be overwritten by children. Vector<uint8_t> get_buffer(int64_t p_length) const; virtual String get_line() const; virtual String get_token() const; @@ -168,7 +168,7 @@ public: virtual Error resize(int64_t p_length) = 0; virtual void flush() = 0; - virtual void store_8(uint8_t p_dest) = 0; ///< store a byte + virtual void store_8(uint8_t p_dest); ///< store a byte virtual void store_16(uint16_t p_dest); ///< store 16 bits uint virtual void store_32(uint32_t p_dest); ///< store 32 bits uint virtual void store_64(uint64_t p_dest); ///< store 64 bits uint @@ -184,7 +184,7 @@ public: virtual void store_pascal_string(const String &p_string); virtual String get_pascal_string(); - virtual void store_buffer(const uint8_t *p_src, uint64_t p_length); ///< store an array of bytes + virtual void store_buffer(const uint8_t *p_src, uint64_t p_length) = 0; ///< store an array of bytes, needs to be overwritten by children. void store_buffer(const Vector<uint8_t> &p_buffer); void store_var(const Variant &p_var, bool p_full_objects = false); diff --git a/core/io/file_access_compressed.cpp b/core/io/file_access_compressed.cpp index 0f00bd292c..3602baf8c5 100644 --- a/core/io/file_access_compressed.cpp +++ b/core/io/file_access_compressed.cpp @@ -260,38 +260,6 @@ bool FileAccessCompressed::eof_reached() const { } } -uint8_t FileAccessCompressed::get_8() const { - ERR_FAIL_COND_V_MSG(f.is_null(), 0, "File must be opened before use."); - ERR_FAIL_COND_V_MSG(writing, 0, "File has not been opened in read mode."); - - if (at_end) { - read_eof = true; - return 0; - } - - uint8_t ret = read_ptr[read_pos]; - - read_pos++; - if (read_pos >= read_block_size) { - read_block++; - - if (read_block < read_block_count) { - //read another block of compressed data - f->get_buffer(comp_buffer.ptrw(), read_blocks[read_block].csize); - int total = Compression::decompress(buffer.ptrw(), read_blocks.size() == 1 ? read_total : block_size, comp_buffer.ptr(), read_blocks[read_block].csize, cmode); - ERR_FAIL_COND_V_MSG(total == -1, 0, "Compressed file is corrupt."); - read_block_size = read_block == read_block_count - 1 ? read_total % block_size : block_size; - read_pos = 0; - - } else { - read_block--; - at_end = true; - } - } - - return ret; -} - uint64_t FileAccessCompressed::get_buffer(uint8_t *p_dst, uint64_t p_length) const { ERR_FAIL_COND_V(!p_dst && p_length > 0, -1); ERR_FAIL_COND_V_MSG(f.is_null(), -1, "File must be opened before use."); @@ -341,12 +309,13 @@ void FileAccessCompressed::flush() { // compressed files keep data in memory till close() } -void FileAccessCompressed::store_8(uint8_t p_dest) { +void FileAccessCompressed::store_buffer(const uint8_t *p_src, uint64_t p_length) { ERR_FAIL_COND_MSG(f.is_null(), "File must be opened before use."); ERR_FAIL_COND_MSG(!writing, "File has not been opened in write mode."); - WRITE_FIT(1); - write_ptr[write_pos++] = p_dest; + WRITE_FIT(p_length); + memcpy(write_ptr + write_pos, p_src, p_length); + write_pos += p_length; } bool FileAccessCompressed::file_exists(const String &p_name) { diff --git a/core/io/file_access_compressed.h b/core/io/file_access_compressed.h index f706c82f8e..ea9837dd03 100644 --- a/core/io/file_access_compressed.h +++ b/core/io/file_access_compressed.h @@ -83,14 +83,13 @@ public: virtual bool eof_reached() const override; ///< reading passed EOF - virtual uint8_t get_8() const override; ///< get a byte virtual uint64_t get_buffer(uint8_t *p_dst, uint64_t p_length) const override; virtual Error get_error() const override; ///< get last error virtual Error resize(int64_t p_length) override { return ERR_UNAVAILABLE; } virtual void flush() override; - virtual void store_8(uint8_t p_dest) override; ///< store a byte + virtual void store_buffer(const uint8_t *p_src, uint64_t p_length) override; virtual bool file_exists(const String &p_name) override; ///< return true if a file exists diff --git a/core/io/file_access_encrypted.cpp b/core/io/file_access_encrypted.cpp index b689f5b628..605c5b2f6f 100644 --- a/core/io/file_access_encrypted.cpp +++ b/core/io/file_access_encrypted.cpp @@ -206,26 +206,13 @@ bool FileAccessEncrypted::eof_reached() const { return eofed; } -uint8_t FileAccessEncrypted::get_8() const { - ERR_FAIL_COND_V_MSG(writing, 0, "File has not been opened in read mode."); - if (pos >= get_length()) { - eofed = true; - return 0; - } - - uint8_t b = data[pos]; - pos++; - return b; -} - uint64_t FileAccessEncrypted::get_buffer(uint8_t *p_dst, uint64_t p_length) const { ERR_FAIL_COND_V(!p_dst && p_length > 0, -1); ERR_FAIL_COND_V_MSG(writing, -1, "File has not been opened in read mode."); uint64_t to_copy = MIN(p_length, get_length() - pos); - for (uint64_t i = 0; i < to_copy; i++) { - p_dst[i] = data[pos++]; - } + memcpy(p_dst, data.ptr() + pos, to_copy); + pos += to_copy; if (to_copy < p_length) { eofed = true; @@ -242,17 +229,12 @@ void FileAccessEncrypted::store_buffer(const uint8_t *p_src, uint64_t p_length) ERR_FAIL_COND_MSG(!writing, "File has not been opened in write mode."); ERR_FAIL_COND(!p_src && p_length > 0); - if (pos < get_length()) { - for (uint64_t i = 0; i < p_length; i++) { - store_8(p_src[i]); - } - } else if (pos == get_length()) { + if (pos + p_length >= get_length()) { data.resize(pos + p_length); - for (uint64_t i = 0; i < p_length; i++) { - data.write[pos + i] = p_src[i]; - } - pos += p_length; } + + memcpy(data.ptrw() + pos, p_src, p_length); + pos += p_length; } void FileAccessEncrypted::flush() { @@ -261,18 +243,6 @@ void FileAccessEncrypted::flush() { // encrypted files keep data in memory till close() } -void FileAccessEncrypted::store_8(uint8_t p_dest) { - ERR_FAIL_COND_MSG(!writing, "File has not been opened in write mode."); - - if (pos < get_length()) { - data.write[pos] = p_dest; - pos++; - } else if (pos == get_length()) { - data.push_back(p_dest); - pos++; - } -} - bool FileAccessEncrypted::file_exists(const String &p_name) { Ref<FileAccess> fa = FileAccess::open(p_name, FileAccess::READ); if (fa.is_null()) { diff --git a/core/io/file_access_encrypted.h b/core/io/file_access_encrypted.h index 42afe49a5e..5f8c803d60 100644 --- a/core/io/file_access_encrypted.h +++ b/core/io/file_access_encrypted.h @@ -73,14 +73,12 @@ public: virtual bool eof_reached() const override; ///< reading passed EOF - virtual uint8_t get_8() const override; ///< get a byte virtual uint64_t get_buffer(uint8_t *p_dst, uint64_t p_length) const override; virtual Error get_error() const override; ///< get last error virtual Error resize(int64_t p_length) override { return ERR_UNAVAILABLE; } virtual void flush() override; - virtual void store_8(uint8_t p_dest) override; ///< store a byte virtual void store_buffer(const uint8_t *p_src, uint64_t p_length) override; ///< store an array of bytes virtual bool file_exists(const String &p_name) override; ///< return true if a file exists diff --git a/core/io/file_access_memory.cpp b/core/io/file_access_memory.cpp index 9521a4f666..1541a5ed4a 100644 --- a/core/io/file_access_memory.cpp +++ b/core/io/file_access_memory.cpp @@ -122,16 +122,6 @@ bool FileAccessMemory::eof_reached() const { return pos >= length; } -uint8_t FileAccessMemory::get_8() const { - uint8_t ret = 0; - if (pos < length) { - ret = data[pos]; - } - ++pos; - - return ret; -} - uint64_t FileAccessMemory::get_buffer(uint8_t *p_dst, uint64_t p_length) const { ERR_FAIL_COND_V(!p_dst && p_length > 0, -1); ERR_FAIL_NULL_V(data, -1); @@ -157,16 +147,12 @@ void FileAccessMemory::flush() { ERR_FAIL_NULL(data); } -void FileAccessMemory::store_8(uint8_t p_byte) { - ERR_FAIL_NULL(data); - ERR_FAIL_COND(pos >= length); - data[pos++] = p_byte; -} - void FileAccessMemory::store_buffer(const uint8_t *p_src, uint64_t p_length) { ERR_FAIL_COND(!p_src && p_length > 0); + uint64_t left = length - pos; uint64_t write = MIN(p_length, left); + if (write < p_length) { WARN_PRINT("Writing less data than requested"); } diff --git a/core/io/file_access_memory.h b/core/io/file_access_memory.h index e9fbc26d75..39e1528d97 100644 --- a/core/io/file_access_memory.h +++ b/core/io/file_access_memory.h @@ -55,15 +55,12 @@ public: virtual bool eof_reached() const override; ///< reading passed EOF - virtual uint8_t get_8() const override; ///< get a byte - virtual uint64_t get_buffer(uint8_t *p_dst, uint64_t p_length) const override; ///< get an array of bytes virtual Error get_error() const override; ///< get last error virtual Error resize(int64_t p_length) override { return ERR_UNAVAILABLE; } virtual void flush() override; - virtual void store_8(uint8_t p_byte) override; ///< store a byte virtual void store_buffer(const uint8_t *p_src, uint64_t p_length) override; ///< store an array of bytes virtual bool file_exists(const String &p_name) override; ///< return true if a file exists diff --git a/core/io/file_access_pack.cpp b/core/io/file_access_pack.cpp index 02bf0a6039..eec27ce0aa 100644 --- a/core/io/file_access_pack.cpp +++ b/core/io/file_access_pack.cpp @@ -313,17 +313,6 @@ bool FileAccessPack::eof_reached() const { return eof; } -uint8_t FileAccessPack::get_8() const { - ERR_FAIL_COND_V_MSG(f.is_null(), 0, "File must be opened before use."); - if (pos >= pf.size) { - eof = true; - return 0; - } - - pos++; - return f->get_8(); -} - uint64_t FileAccessPack::get_buffer(uint8_t *p_dst, uint64_t p_length) const { ERR_FAIL_COND_V_MSG(f.is_null(), -1, "File must be opened before use."); ERR_FAIL_COND_V(!p_dst && p_length > 0, -1); @@ -366,10 +355,6 @@ void FileAccessPack::flush() { ERR_FAIL(); } -void FileAccessPack::store_8(uint8_t p_dest) { - ERR_FAIL(); -} - void FileAccessPack::store_buffer(const uint8_t *p_src, uint64_t p_length) { ERR_FAIL(); } diff --git a/core/io/file_access_pack.h b/core/io/file_access_pack.h index 594ac8f089..595a36bca4 100644 --- a/core/io/file_access_pack.h +++ b/core/io/file_access_pack.h @@ -169,8 +169,6 @@ public: virtual bool eof_reached() const override; - virtual uint8_t get_8() const override; - virtual uint64_t get_buffer(uint8_t *p_dst, uint64_t p_length) const override; virtual void set_big_endian(bool p_big_endian) override; @@ -179,8 +177,6 @@ public: virtual Error resize(int64_t p_length) override { return ERR_UNAVAILABLE; } virtual void flush() override; - virtual void store_8(uint8_t p_dest) override; - virtual void store_buffer(const uint8_t *p_src, uint64_t p_length) override; virtual bool file_exists(const String &p_name) override; diff --git a/core/io/file_access_zip.cpp b/core/io/file_access_zip.cpp index c0d1afc8e1..b33b7b35c3 100644 --- a/core/io/file_access_zip.cpp +++ b/core/io/file_access_zip.cpp @@ -291,12 +291,6 @@ bool FileAccessZip::eof_reached() const { return at_eof; } -uint8_t FileAccessZip::get_8() const { - uint8_t ret = 0; - get_buffer(&ret, 1); - return ret; -} - uint64_t FileAccessZip::get_buffer(uint8_t *p_dst, uint64_t p_length) const { ERR_FAIL_COND_V(!p_dst && p_length > 0, -1); ERR_FAIL_NULL_V(zfile, -1); @@ -328,7 +322,7 @@ void FileAccessZip::flush() { ERR_FAIL(); } -void FileAccessZip::store_8(uint8_t p_dest) { +void FileAccessZip::store_buffer(const uint8_t *p_src, uint64_t p_length) { ERR_FAIL(); } diff --git a/core/io/file_access_zip.h b/core/io/file_access_zip.h index 88b63e93e2..1e11e050df 100644 --- a/core/io/file_access_zip.h +++ b/core/io/file_access_zip.h @@ -95,14 +95,13 @@ public: virtual bool eof_reached() const override; ///< reading passed EOF - virtual uint8_t get_8() const override; ///< get a byte virtual uint64_t get_buffer(uint8_t *p_dst, uint64_t p_length) const override; virtual Error get_error() const override; ///< get last error virtual Error resize(int64_t p_length) override { return ERR_UNAVAILABLE; } virtual void flush() override; - virtual void store_8(uint8_t p_dest) override; ///< store a byte + virtual void store_buffer(const uint8_t *p_src, uint64_t p_length) override; virtual bool file_exists(const String &p_name) override; ///< return true if a file exists diff --git a/core/io/http_client.cpp b/core/io/http_client.cpp index 833fd1adc3..fc91341bed 100644 --- a/core/io/http_client.cpp +++ b/core/io/http_client.cpp @@ -42,9 +42,9 @@ const char *HTTPClient::_methods[METHOD_MAX] = { "PATCH" }; -HTTPClient *HTTPClient::create() { +HTTPClient *HTTPClient::create(bool p_notify_postinitialize) { if (_create) { - return _create(); + return _create(p_notify_postinitialize); } return nullptr; } diff --git a/core/io/http_client.h b/core/io/http_client.h index 9e018182e3..5945291122 100644 --- a/core/io/http_client.h +++ b/core/io/http_client.h @@ -158,12 +158,12 @@ protected: Error _request_raw(Method p_method, const String &p_url, const Vector<String> &p_headers, const Vector<uint8_t> &p_body); Error _request(Method p_method, const String &p_url, const Vector<String> &p_headers, const String &p_body = String()); - static HTTPClient *(*_create)(); + static HTTPClient *(*_create)(bool p_notify_postinitialize); static void _bind_methods(); public: - static HTTPClient *create(); + static HTTPClient *create(bool p_notify_postinitialize = true); String query_string_from_dict(const Dictionary &p_dict); Error verify_headers(const Vector<String> &p_headers); diff --git a/core/io/http_client_tcp.cpp b/core/io/http_client_tcp.cpp index 2f45238951..70fcad543a 100644 --- a/core/io/http_client_tcp.cpp +++ b/core/io/http_client_tcp.cpp @@ -35,8 +35,8 @@ #include "core/io/stream_peer_tls.h" #include "core/version.h" -HTTPClient *HTTPClientTCP::_create_func() { - return memnew(HTTPClientTCP); +HTTPClient *HTTPClientTCP::_create_func(bool p_notify_postinitialize) { + return static_cast<HTTPClient *>(ClassDB::creator<HTTPClientTCP>(p_notify_postinitialize)); } Error HTTPClientTCP::connect_to_host(const String &p_host, int p_port, Ref<TLSOptions> p_options) { @@ -792,6 +792,6 @@ HTTPClientTCP::HTTPClientTCP() { request_buffer.instantiate(); } -HTTPClient *(*HTTPClient::_create)() = HTTPClientTCP::_create_func; +HTTPClient *(*HTTPClient::_create)(bool p_notify_postinitialize) = HTTPClientTCP::_create_func; #endif // WEB_ENABLED diff --git a/core/io/http_client_tcp.h b/core/io/http_client_tcp.h index 6060c975bc..dd6cc6b84f 100644 --- a/core/io/http_client_tcp.h +++ b/core/io/http_client_tcp.h @@ -76,7 +76,7 @@ private: Error _get_http_data(uint8_t *p_buffer, int p_bytes, int &r_received); public: - static HTTPClient *_create_func(); + static HTTPClient *_create_func(bool p_notify_postinitialize); Error request(Method p_method, const String &p_url, const Vector<String> &p_headers, const uint8_t *p_body, int p_body_size) override; diff --git a/core/io/image.cpp b/core/io/image.cpp index 003646d095..f6065d984b 100644 --- a/core/io/image.cpp +++ b/core/io/image.cpp @@ -3824,6 +3824,33 @@ void Image::bump_map_to_normal_map(float bump_scale) { data = result_image; } +bool Image::detect_signed(bool p_include_mips) const { + ERR_FAIL_COND_V(is_compressed(), false); + + if (format >= Image::FORMAT_RH && format <= Image::FORMAT_RGBAH) { + const uint16_t *img_data = reinterpret_cast<const uint16_t *>(data.ptr()); + const uint64_t img_size = p_include_mips ? (data.size() / 2) : (width * height * get_format_pixel_size(format) / 2); + + for (uint64_t i = 0; i < img_size; i++) { + if ((img_data[i] & 0x8000) != 0 && (img_data[i] & 0x7fff) != 0) { + return true; + } + } + + } else if (format >= Image::FORMAT_RF && format <= Image::FORMAT_RGBAF) { + const uint32_t *img_data = reinterpret_cast<const uint32_t *>(data.ptr()); + const uint64_t img_size = p_include_mips ? (data.size() / 4) : (width * height * get_format_pixel_size(format) / 4); + + for (uint64_t i = 0; i < img_size; i++) { + if ((img_data[i] & 0x80000000) != 0 && (img_data[i] & 0x7fffffff) != 0) { + return true; + } + } + } + + return false; +} + void Image::srgb_to_linear() { if (data.size() == 0) { return; diff --git a/core/io/image.h b/core/io/image.h index 8d09a0b40b..4461ae71a6 100644 --- a/core/io/image.h +++ b/core/io/image.h @@ -391,6 +391,8 @@ public: Ref<Image> get_image_from_mipmap(int p_mipmap) const; void bump_map_to_normal_map(float bump_scale = 1.0); + bool detect_signed(bool p_include_mips = true) const; + void blit_rect(const Ref<Image> &p_src, const Rect2i &p_src_rect, const Point2i &p_dest); void blit_rect_mask(const Ref<Image> &p_src, const Ref<Image> &p_mask, const Rect2i &p_src_rect, const Point2i &p_dest); void blend_rect(const Ref<Image> &p_src, const Rect2i &p_src_rect, const Point2i &p_dest); diff --git a/core/io/ip.cpp b/core/io/ip.cpp index f20d65bef9..38c71b19fa 100644 --- a/core/io/ip.cpp +++ b/core/io/ip.cpp @@ -81,17 +81,17 @@ struct _IP_ResolverPrivate { continue; } - mutex.lock(); + MutexLock lock(mutex); List<IPAddress> response; String hostname = queue[i].hostname; IP::Type type = queue[i].type; - mutex.unlock(); + lock.temp_unlock(); // We should not lock while resolving the hostname, // only when modifying the queue. IP::get_singleton()->_resolve_hostname(response, hostname, type); - MutexLock lock(mutex); + lock.temp_relock(); // Could have been completed by another function, or deleted. if (queue[i].status.get() != IP::RESOLVER_STATUS_WAITING) { continue; @@ -131,21 +131,22 @@ PackedStringArray IP::resolve_hostname_addresses(const String &p_hostname, Type List<IPAddress> res; String key = _IP_ResolverPrivate::get_cache_key(p_hostname, p_type); - resolver->mutex.lock(); - if (resolver->cache.has(key)) { - res = resolver->cache[key]; - } else { - // This should be run unlocked so the resolver thread can keep resolving - // other requests. - resolver->mutex.unlock(); - _resolve_hostname(res, p_hostname, p_type); - resolver->mutex.lock(); - // We might be overriding another result, but we don't care as long as the result is valid. - if (res.size()) { - resolver->cache[key] = res; + { + MutexLock lock(resolver->mutex); + if (resolver->cache.has(key)) { + res = resolver->cache[key]; + } else { + // This should be run unlocked so the resolver thread can keep resolving + // other requests. + lock.temp_unlock(); + _resolve_hostname(res, p_hostname, p_type); + lock.temp_relock(); + // We might be overriding another result, but we don't care as long as the result is valid. + if (res.size()) { + resolver->cache[key] = res; + } } } - resolver->mutex.unlock(); PackedStringArray result; for (const IPAddress &E : res) { diff --git a/core/io/json.cpp b/core/io/json.cpp index 61051727c1..664ff7857b 100644 --- a/core/io/json.cpp +++ b/core/io/json.cpp @@ -588,10 +588,756 @@ void JSON::_bind_methods() { ClassDB::bind_method(D_METHOD("get_error_line"), &JSON::get_error_line); ClassDB::bind_method(D_METHOD("get_error_message"), &JSON::get_error_message); + ClassDB::bind_static_method("JSON", D_METHOD("to_native", "json", "allow_classes", "allow_scripts"), &JSON::to_native, DEFVAL(false), DEFVAL(false)); + ClassDB::bind_static_method("JSON", D_METHOD("from_native", "variant", "allow_classes", "allow_scripts"), &JSON::from_native, DEFVAL(false), DEFVAL(false)); + ADD_PROPERTY(PropertyInfo(Variant::NIL, "data", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_NIL_IS_VARIANT), "set_data", "get_data"); // Ensures that it can be serialized as binary. } -//// +#define GDTYPE "__gdtype" +#define VALUES "values" +#define PASS_ARG p_allow_classes, p_allow_scripts + +Variant JSON::from_native(const Variant &p_variant, bool p_allow_classes, bool p_allow_scripts) { + switch (p_variant.get_type()) { + case Variant::NIL: { + Dictionary nil; + nil[GDTYPE] = Variant::get_type_name(p_variant.get_type()); + return nil; + } break; + case Variant::BOOL: { + return p_variant; + } break; + case Variant::INT: { + return p_variant; + } break; + case Variant::FLOAT: { + return p_variant; + } break; + case Variant::STRING: { + return p_variant; + } break; + case Variant::VECTOR2: { + Dictionary d; + Vector2 v = p_variant; + Array values; + values.push_back(v.x); + values.push_back(v.y); + d[VALUES] = values; + d[GDTYPE] = Variant::get_type_name(p_variant.get_type()); + return d; + } break; + case Variant::VECTOR2I: { + Dictionary d; + Vector2i v = p_variant; + Array values; + values.push_back(v.x); + values.push_back(v.y); + d[VALUES] = values; + d[GDTYPE] = Variant::get_type_name(p_variant.get_type()); + return d; + } break; + case Variant::RECT2: { + Dictionary d; + Rect2 r = p_variant; + d["position"] = from_native(r.position); + d["size"] = from_native(r.size); + d[GDTYPE] = Variant::get_type_name(p_variant.get_type()); + return d; + } break; + case Variant::RECT2I: { + Dictionary d; + Rect2i r = p_variant; + d["position"] = from_native(r.position); + d["size"] = from_native(r.size); + d[GDTYPE] = Variant::get_type_name(p_variant.get_type()); + return d; + } break; + case Variant::VECTOR3: { + Dictionary d; + Vector3 v = p_variant; + Array values; + values.push_back(v.x); + values.push_back(v.y); + values.push_back(v.z); + d[VALUES] = values; + d[GDTYPE] = Variant::get_type_name(p_variant.get_type()); + return d; + } break; + case Variant::VECTOR3I: { + Dictionary d; + Vector3i v = p_variant; + Array values; + values.push_back(v.x); + values.push_back(v.y); + values.push_back(v.z); + d[VALUES] = values; + d[GDTYPE] = Variant::get_type_name(p_variant.get_type()); + return d; + } break; + case Variant::TRANSFORM2D: { + Dictionary d; + Transform2D t = p_variant; + d["x"] = from_native(t[0]); + d["y"] = from_native(t[1]); + d["origin"] = from_native(t[2]); + d[GDTYPE] = Variant::get_type_name(p_variant.get_type()); + return d; + } break; + case Variant::VECTOR4: { + Dictionary d; + Vector4 v = p_variant; + Array values; + values.push_back(v.x); + values.push_back(v.y); + values.push_back(v.z); + values.push_back(v.w); + d[VALUES] = values; + d[GDTYPE] = Variant::get_type_name(p_variant.get_type()); + return d; + } break; + case Variant::VECTOR4I: { + Dictionary d; + Vector4i v = p_variant; + Array values; + values.push_back(v.x); + values.push_back(v.y); + values.push_back(v.z); + values.push_back(v.w); + d[VALUES] = values; + d[GDTYPE] = Variant::get_type_name(p_variant.get_type()); + return d; + } break; + case Variant::PLANE: { + Dictionary d; + Plane p = p_variant; + d["normal"] = from_native(p.normal); + d["d"] = p.d; + d[GDTYPE] = Variant::get_type_name(p_variant.get_type()); + return d; + } break; + case Variant::QUATERNION: { + Dictionary d; + Quaternion q = p_variant; + Array values; + values.push_back(q.x); + values.push_back(q.y); + values.push_back(q.z); + values.push_back(q.w); + d[VALUES] = values; + d[GDTYPE] = Variant::get_type_name(p_variant.get_type()); + return d; + } break; + case Variant::AABB: { + Dictionary d; + AABB aabb = p_variant; + d["position"] = from_native(aabb.position); + d["size"] = from_native(aabb.size); + d[GDTYPE] = Variant::get_type_name(p_variant.get_type()); + return d; + } break; + case Variant::BASIS: { + Dictionary d; + Basis t = p_variant; + d["x"] = from_native(t.get_column(0)); + d["y"] = from_native(t.get_column(1)); + d["z"] = from_native(t.get_column(2)); + d[GDTYPE] = Variant::get_type_name(p_variant.get_type()); + return d; + } break; + case Variant::TRANSFORM3D: { + Dictionary d; + Transform3D t = p_variant; + d["basis"] = from_native(t.basis); + d["origin"] = from_native(t.origin); + d[GDTYPE] = Variant::get_type_name(p_variant.get_type()); + return d; + } break; + case Variant::PROJECTION: { + Dictionary d; + Projection t = p_variant; + d["x"] = from_native(t[0]); + d["y"] = from_native(t[1]); + d["z"] = from_native(t[2]); + d["w"] = from_native(t[3]); + d[GDTYPE] = Variant::get_type_name(p_variant.get_type()); + return d; + } break; + case Variant::COLOR: { + Dictionary d; + Color c = p_variant; + Array values; + values.push_back(c.r); + values.push_back(c.g); + values.push_back(c.b); + values.push_back(c.a); + d[VALUES] = values; + d[GDTYPE] = Variant::get_type_name(p_variant.get_type()); + return d; + } break; + case Variant::STRING_NAME: { + Dictionary d; + d["name"] = String(p_variant); + d[GDTYPE] = Variant::get_type_name(p_variant.get_type()); + return d; + } break; + case Variant::NODE_PATH: { + Dictionary d; + d["path"] = String(p_variant); + d[GDTYPE] = Variant::get_type_name(p_variant.get_type()); + return d; + } break; + case Variant::RID: { + Dictionary d; + d[GDTYPE] = Variant::get_type_name(p_variant.get_type()); + return d; + } break; + case Variant::OBJECT: { + Object *obj = p_variant.get_validated_object(); + + if (p_allow_classes && obj) { + Dictionary d; + List<PropertyInfo> property_list; + obj->get_property_list(&property_list); + + d["type"] = obj->get_class(); + Dictionary p; + for (const PropertyInfo &P : property_list) { + if (P.usage & PROPERTY_USAGE_STORAGE) { + if (P.name == "script" && !p_allow_scripts) { + continue; + } + p[P.name] = from_native(obj->get(P.name), PASS_ARG); + } + } + d["properties"] = p; + d[GDTYPE] = Variant::get_type_name(p_variant.get_type()); + return d; + } else { + Dictionary nil; + nil[GDTYPE] = Variant::get_type_name(p_variant.get_type()); + return nil; + } + } break; + case Variant::CALLABLE: + case Variant::SIGNAL: { + Dictionary nil; + nil[GDTYPE] = Variant::get_type_name(p_variant.get_type()); + return nil; + } break; + case Variant::DICTIONARY: { + Dictionary d = p_variant; + List<Variant> keys; + d.get_key_list(&keys); + bool all_strings = true; + for (const Variant &K : keys) { + if (K.get_type() != Variant::STRING) { + all_strings = false; + break; + } + } + + if (all_strings) { + Dictionary ret_dict; + for (const Variant &K : keys) { + ret_dict[K] = from_native(d[K], PASS_ARG); + } + return ret_dict; + } else { + Dictionary ret; + Array pairs; + for (const Variant &K : keys) { + Dictionary pair; + pair["key"] = from_native(K, PASS_ARG); + pair["value"] = from_native(d[K], PASS_ARG); + pairs.push_back(pair); + } + ret["pairs"] = pairs; + ret[GDTYPE] = Variant::get_type_name(p_variant.get_type()); + return ret; + } + } break; + case Variant::ARRAY: { + Array arr = p_variant; + Array ret; + for (int i = 0; i < arr.size(); i++) { + ret.push_back(from_native(arr[i], PASS_ARG)); + } + return ret; + } break; + case Variant::PACKED_BYTE_ARRAY: { + Dictionary d; + PackedByteArray arr = p_variant; + Array values; + for (int i = 0; i < arr.size(); i++) { + values.push_back(arr[i]); + } + d[VALUES] = values; + d[GDTYPE] = Variant::get_type_name(p_variant.get_type()); + return d; + } break; + case Variant::PACKED_INT32_ARRAY: { + Dictionary d; + PackedInt32Array arr = p_variant; + Array values; + for (int i = 0; i < arr.size(); i++) { + values.push_back(arr[i]); + } + d[VALUES] = values; + d[GDTYPE] = Variant::get_type_name(p_variant.get_type()); + return d; + + } break; + case Variant::PACKED_INT64_ARRAY: { + Dictionary d; + PackedInt64Array arr = p_variant; + Array values; + for (int i = 0; i < arr.size(); i++) { + values.push_back(arr[i]); + } + d[VALUES] = values; + d[GDTYPE] = Variant::get_type_name(p_variant.get_type()); + return d; + } break; + case Variant::PACKED_FLOAT32_ARRAY: { + Dictionary d; + PackedFloat32Array arr = p_variant; + Array values; + for (int i = 0; i < arr.size(); i++) { + values.push_back(arr[i]); + } + d[VALUES] = values; + d[GDTYPE] = Variant::get_type_name(p_variant.get_type()); + return d; + } break; + case Variant::PACKED_FLOAT64_ARRAY: { + Dictionary d; + PackedFloat64Array arr = p_variant; + Array values; + for (int i = 0; i < arr.size(); i++) { + values.push_back(arr[i]); + } + d[VALUES] = values; + d[GDTYPE] = Variant::get_type_name(p_variant.get_type()); + return d; + } break; + case Variant::PACKED_STRING_ARRAY: { + Dictionary d; + PackedStringArray arr = p_variant; + Array values; + for (int i = 0; i < arr.size(); i++) { + values.push_back(arr[i]); + } + d[VALUES] = values; + d[GDTYPE] = Variant::get_type_name(p_variant.get_type()); + return d; + } break; + case Variant::PACKED_VECTOR2_ARRAY: { + Dictionary d; + PackedVector2Array arr = p_variant; + Array values; + for (int i = 0; i < arr.size(); i++) { + Vector2 v = arr[i]; + values.push_back(v.x); + values.push_back(v.y); + } + d[VALUES] = values; + d[GDTYPE] = Variant::get_type_name(p_variant.get_type()); + return d; + } break; + case Variant::PACKED_VECTOR3_ARRAY: { + Dictionary d; + PackedVector3Array arr = p_variant; + Array values; + for (int i = 0; i < arr.size(); i++) { + Vector3 v = arr[i]; + values.push_back(v.x); + values.push_back(v.y); + values.push_back(v.z); + } + d[VALUES] = values; + d[GDTYPE] = Variant::get_type_name(p_variant.get_type()); + return d; + } break; + case Variant::PACKED_COLOR_ARRAY: { + Dictionary d; + PackedColorArray arr = p_variant; + Array values; + for (int i = 0; i < arr.size(); i++) { + Color v = arr[i]; + values.push_back(v.r); + values.push_back(v.g); + values.push_back(v.b); + values.push_back(v.a); + } + d[VALUES] = values; + d[GDTYPE] = Variant::get_type_name(p_variant.get_type()); + return d; + } break; + case Variant::PACKED_VECTOR4_ARRAY: { + Dictionary d; + PackedVector4Array arr = p_variant; + Array values; + for (int i = 0; i < arr.size(); i++) { + Vector4 v = arr[i]; + values.push_back(v.x); + values.push_back(v.y); + values.push_back(v.z); + values.push_back(v.w); + } + d[VALUES] = values; + d[GDTYPE] = Variant::get_type_name(p_variant.get_type()); + return d; + } break; + default: { + ERR_PRINT(vformat("Unhandled conversion from native Variant type '%s' to JSON.", Variant::get_type_name(p_variant.get_type()))); + } break; + } + + Dictionary nil; + nil[GDTYPE] = Variant::get_type_name(p_variant.get_type()); + return nil; +} + +Variant JSON::to_native(const Variant &p_json, bool p_allow_classes, bool p_allow_scripts) { + switch (p_json.get_type()) { + case Variant::BOOL: { + return p_json; + } break; + case Variant::INT: { + return p_json; + } break; + case Variant::FLOAT: { + return p_json; + } break; + case Variant::STRING: { + return p_json; + } break; + case Variant::STRING_NAME: { + return p_json; + } break; + case Variant::CALLABLE: { + return p_json; + } break; + case Variant::DICTIONARY: { + Dictionary d = p_json; + if (d.has(GDTYPE)) { + // Specific Godot Variant types serialized to JSON. + String type = d[GDTYPE]; + if (type == Variant::get_type_name(Variant::VECTOR2)) { + ERR_FAIL_COND_V(!d.has(VALUES), Variant()); + Array values = d[VALUES]; + ERR_FAIL_COND_V(values.size() != 2, Variant()); + Vector2 v; + v.x = values[0]; + v.y = values[1]; + return v; + } else if (type == Variant::get_type_name(Variant::VECTOR2I)) { + ERR_FAIL_COND_V(!d.has(VALUES), Variant()); + Array values = d[VALUES]; + ERR_FAIL_COND_V(values.size() != 2, Variant()); + Vector2i v; + v.x = values[0]; + v.y = values[1]; + return v; + } else if (type == Variant::get_type_name(Variant::RECT2)) { + ERR_FAIL_COND_V(!d.has("position"), Variant()); + ERR_FAIL_COND_V(!d.has("size"), Variant()); + Rect2 r; + r.position = to_native(d["position"]); + r.size = to_native(d["size"]); + return r; + } else if (type == Variant::get_type_name(Variant::RECT2I)) { + ERR_FAIL_COND_V(!d.has("position"), Variant()); + ERR_FAIL_COND_V(!d.has("size"), Variant()); + Rect2i r; + r.position = to_native(d["position"]); + r.size = to_native(d["size"]); + return r; + } else if (type == Variant::get_type_name(Variant::VECTOR3)) { + ERR_FAIL_COND_V(!d.has(VALUES), Variant()); + Array values = d[VALUES]; + ERR_FAIL_COND_V(values.size() != 3, Variant()); + Vector3 v; + v.x = values[0]; + v.y = values[1]; + v.z = values[2]; + return v; + } else if (type == Variant::get_type_name(Variant::VECTOR3I)) { + ERR_FAIL_COND_V(!d.has(VALUES), Variant()); + Array values = d[VALUES]; + ERR_FAIL_COND_V(values.size() != 3, Variant()); + Vector3i v; + v.x = values[0]; + v.y = values[1]; + v.z = values[2]; + return v; + } else if (type == Variant::get_type_name(Variant::TRANSFORM2D)) { + ERR_FAIL_COND_V(!d.has("x"), Variant()); + ERR_FAIL_COND_V(!d.has("y"), Variant()); + ERR_FAIL_COND_V(!d.has("origin"), Variant()); + Transform2D t; + t[0] = to_native(d["x"]); + t[1] = to_native(d["y"]); + t[2] = to_native(d["origin"]); + return t; + } else if (type == Variant::get_type_name(Variant::VECTOR4)) { + ERR_FAIL_COND_V(!d.has(VALUES), Variant()); + Array values = d[VALUES]; + ERR_FAIL_COND_V(values.size() != 4, Variant()); + Vector4 v; + v.x = values[0]; + v.y = values[1]; + v.z = values[2]; + v.w = values[3]; + return v; + } else if (type == Variant::get_type_name(Variant::VECTOR4I)) { + ERR_FAIL_COND_V(!d.has(VALUES), Variant()); + Array values = d[VALUES]; + ERR_FAIL_COND_V(values.size() != 4, Variant()); + Vector4i v; + v.x = values[0]; + v.y = values[1]; + v.z = values[2]; + v.w = values[3]; + return v; + } else if (type == Variant::get_type_name(Variant::PLANE)) { + ERR_FAIL_COND_V(!d.has("normal"), Variant()); + ERR_FAIL_COND_V(!d.has("d"), Variant()); + Plane p; + p.normal = to_native(d["normal"]); + p.d = d["d"]; + return p; + } else if (type == Variant::get_type_name(Variant::QUATERNION)) { + ERR_FAIL_COND_V(!d.has(VALUES), Variant()); + Array values = d[VALUES]; + ERR_FAIL_COND_V(values.size() != 4, Variant()); + Quaternion v; + v.x = values[0]; + v.y = values[1]; + v.z = values[2]; + v.w = values[3]; + return v; + } else if (type == Variant::get_type_name(Variant::AABB)) { + ERR_FAIL_COND_V(!d.has("position"), Variant()); + ERR_FAIL_COND_V(!d.has("size"), Variant()); + AABB r; + r.position = to_native(d["position"]); + r.size = to_native(d["size"]); + return r; + } else if (type == Variant::get_type_name(Variant::BASIS)) { + ERR_FAIL_COND_V(!d.has("x"), Variant()); + ERR_FAIL_COND_V(!d.has("y"), Variant()); + ERR_FAIL_COND_V(!d.has("z"), Variant()); + Basis b; + b.set_column(0, to_native(d["x"])); + b.set_column(1, to_native(d["y"])); + b.set_column(2, to_native(d["z"])); + return b; + } else if (type == Variant::get_type_name(Variant::TRANSFORM3D)) { + ERR_FAIL_COND_V(!d.has("basis"), Variant()); + ERR_FAIL_COND_V(!d.has("origin"), Variant()); + Transform3D t; + t.basis = to_native(d["basis"]); + t.origin = to_native(d["origin"]); + return t; + } else if (type == Variant::get_type_name(Variant::PROJECTION)) { + ERR_FAIL_COND_V(!d.has("x"), Variant()); + ERR_FAIL_COND_V(!d.has("y"), Variant()); + ERR_FAIL_COND_V(!d.has("z"), Variant()); + ERR_FAIL_COND_V(!d.has("w"), Variant()); + Projection p; + p[0] = to_native(d["x"]); + p[1] = to_native(d["y"]); + p[2] = to_native(d["z"]); + p[3] = to_native(d["w"]); + return p; + } else if (type == Variant::get_type_name(Variant::COLOR)) { + ERR_FAIL_COND_V(!d.has(VALUES), Variant()); + Array values = d[VALUES]; + ERR_FAIL_COND_V(values.size() != 4, Variant()); + Color c; + c.r = values[0]; + c.g = values[1]; + c.b = values[2]; + c.a = values[3]; + return c; + } else if (type == Variant::get_type_name(Variant::NODE_PATH)) { + ERR_FAIL_COND_V(!d.has("path"), Variant()); + NodePath np = d["path"]; + return np; + } else if (type == Variant::get_type_name(Variant::STRING_NAME)) { + ERR_FAIL_COND_V(!d.has("name"), Variant()); + StringName s = d["name"]; + return s; + } else if (type == Variant::get_type_name(Variant::OBJECT)) { + ERR_FAIL_COND_V(!d.has("type"), Variant()); + ERR_FAIL_COND_V(!d.has("properties"), Variant()); + + ERR_FAIL_COND_V(!p_allow_classes, Variant()); + + String obj_type = d["type"]; + bool is_script = obj_type == "Script" || ClassDB::is_parent_class(obj_type, "Script"); + ERR_FAIL_COND_V(!p_allow_scripts && is_script, Variant()); + Object *obj = ClassDB::instantiate(obj_type); + ERR_FAIL_NULL_V(obj, Variant()); + + Dictionary p = d["properties"]; + + List<Variant> keys; + p.get_key_list(&keys); + + for (const Variant &K : keys) { + String property = K; + Variant value = to_native(p[K], PASS_ARG); + obj->set(property, value); + } + + Variant v(obj); + + return v; + } else if (type == Variant::get_type_name(Variant::DICTIONARY)) { + ERR_FAIL_COND_V(!d.has("pairs"), Variant()); + Array pairs = d["pairs"]; + Dictionary r; + for (int i = 0; i < pairs.size(); i++) { + Dictionary p = pairs[i]; + ERR_CONTINUE(!p.has("key")); + ERR_CONTINUE(!p.has("value")); + r[to_native(p["key"], PASS_ARG)] = to_native(p["value"]); + } + return r; + } else if (type == Variant::get_type_name(Variant::ARRAY)) { + ERR_PRINT(vformat("Unexpected Array with '%s' key. Arrays are supported natively.", GDTYPE)); + } else if (type == Variant::get_type_name(Variant::PACKED_BYTE_ARRAY)) { + ERR_FAIL_COND_V(!d.has(VALUES), Variant()); + Array values = d[VALUES]; + PackedByteArray pbarr; + pbarr.resize(values.size()); + for (int i = 0; i < pbarr.size(); i++) { + pbarr.write[i] = values[i]; + } + return pbarr; + } else if (type == Variant::get_type_name(Variant::PACKED_INT32_ARRAY)) { + ERR_FAIL_COND_V(!d.has(VALUES), Variant()); + Array values = d[VALUES]; + PackedInt32Array arr; + arr.resize(values.size()); + for (int i = 0; i < arr.size(); i++) { + arr.write[i] = values[i]; + } + return arr; + } else if (type == Variant::get_type_name(Variant::PACKED_INT64_ARRAY)) { + ERR_FAIL_COND_V(!d.has(VALUES), Variant()); + Array values = d[VALUES]; + PackedInt64Array arr; + arr.resize(values.size()); + for (int i = 0; i < arr.size(); i++) { + arr.write[i] = values[i]; + } + return arr; + } else if (type == Variant::get_type_name(Variant::PACKED_FLOAT32_ARRAY)) { + ERR_FAIL_COND_V(!d.has(VALUES), Variant()); + Array values = d[VALUES]; + PackedFloat32Array arr; + arr.resize(values.size()); + for (int i = 0; i < arr.size(); i++) { + arr.write[i] = values[i]; + } + return arr; + } else if (type == Variant::get_type_name(Variant::PACKED_FLOAT64_ARRAY)) { + ERR_FAIL_COND_V(!d.has(VALUES), Variant()); + Array values = d[VALUES]; + PackedFloat64Array arr; + arr.resize(values.size()); + for (int i = 0; i < arr.size(); i++) { + arr.write[i] = values[i]; + } + return arr; + } else if (type == Variant::get_type_name(Variant::PACKED_STRING_ARRAY)) { + ERR_FAIL_COND_V(!d.has(VALUES), Variant()); + Array values = d[VALUES]; + PackedStringArray arr; + arr.resize(values.size()); + for (int i = 0; i < arr.size(); i++) { + arr.write[i] = values[i]; + } + return arr; + } else if (type == Variant::get_type_name(Variant::PACKED_VECTOR2_ARRAY)) { + ERR_FAIL_COND_V(!d.has(VALUES), Variant()); + Array values = d[VALUES]; + ERR_FAIL_COND_V(values.size() % 2 != 0, Variant()); + PackedVector2Array arr; + arr.resize(values.size() / 2); + for (int i = 0; i < arr.size(); i++) { + arr.write[i] = Vector2(values[i * 2 + 0], values[i * 2 + 1]); + } + return arr; + } else if (type == Variant::get_type_name(Variant::PACKED_VECTOR3_ARRAY)) { + ERR_FAIL_COND_V(!d.has(VALUES), Variant()); + Array values = d[VALUES]; + ERR_FAIL_COND_V(values.size() % 3 != 0, Variant()); + PackedVector3Array arr; + arr.resize(values.size() / 3); + for (int i = 0; i < arr.size(); i++) { + arr.write[i] = Vector3(values[i * 3 + 0], values[i * 3 + 1], values[i * 3 + 2]); + } + return arr; + } else if (type == Variant::get_type_name(Variant::PACKED_COLOR_ARRAY)) { + ERR_FAIL_COND_V(!d.has(VALUES), Variant()); + Array values = d[VALUES]; + ERR_FAIL_COND_V(values.size() % 4 != 0, Variant()); + PackedColorArray arr; + arr.resize(values.size() / 4); + for (int i = 0; i < arr.size(); i++) { + arr.write[i] = Color(values[i * 4 + 0], values[i * 4 + 1], values[i * 4 + 2], values[i * 4 + 3]); + } + return arr; + } else if (type == Variant::get_type_name(Variant::PACKED_VECTOR4_ARRAY)) { + ERR_FAIL_COND_V(!d.has(VALUES), Variant()); + Array values = d[VALUES]; + ERR_FAIL_COND_V(values.size() % 4 != 0, Variant()); + PackedVector4Array arr; + arr.resize(values.size() / 4); + for (int i = 0; i < arr.size(); i++) { + arr.write[i] = Vector4(values[i * 4 + 0], values[i * 4 + 1], values[i * 4 + 2], values[i * 4 + 3]); + } + return arr; + } else { + return Variant(); + } + } else { + // Regular dictionary with string keys. + List<Variant> keys; + d.get_key_list(&keys); + Dictionary r; + for (const Variant &K : keys) { + r[K] = to_native(d[K], PASS_ARG); + } + return r; + } + } break; + case Variant::ARRAY: { + Array arr = p_json; + Array ret; + ret.resize(arr.size()); + for (int i = 0; i < arr.size(); i++) { + ret[i] = to_native(arr[i], PASS_ARG); + } + return ret; + } break; + default: { + ERR_PRINT(vformat("Unhandled conversion from JSON type '%s' to native Variant type.", Variant::get_type_name(p_json.get_type()))); + return Variant(); + } + } + + return Variant(); +} + +#undef GDTYPE +#undef VALUES +#undef PASS_ARG //////////// diff --git a/core/io/json.h b/core/io/json.h index 801fa29b4b..67b5e09afa 100644 --- a/core/io/json.h +++ b/core/io/json.h @@ -94,6 +94,9 @@ public: void set_data(const Variant &p_data); inline int get_error_line() const { return err_line; } inline String get_error_message() const { return err_str; } + + static Variant from_native(const Variant &p_variant, bool p_allow_classes = false, bool p_allow_scripts = false); + static Variant to_native(const Variant &p_json, bool p_allow_classes = false, bool p_allow_scripts = false); }; class ResourceFormatLoaderJSON : public ResourceFormatLoader { diff --git a/core/io/logger.cpp b/core/io/logger.cpp index a24277fe72..26b60f6738 100644 --- a/core/io/logger.cpp +++ b/core/io/logger.cpp @@ -84,11 +84,7 @@ void Logger::log_error(const char *p_function, const char *p_file, int p_line, c err_details = p_code; } - if (p_editor_notify) { - logf_error("%s: %s\n", err_type, err_details); - } else { - logf_error("USER %s: %s\n", err_type, err_details); - } + logf_error("%s: %s\n", err_type, err_details); logf_error(" at: %s (%s:%i)\n", p_function, p_file, p_line); } diff --git a/core/io/packed_data_container.h b/core/io/packed_data_container.h index cc9996101e..f4ffa09022 100644 --- a/core/io/packed_data_container.h +++ b/core/io/packed_data_container.h @@ -36,7 +36,7 @@ class PackedDataContainer : public Resource { GDCLASS(PackedDataContainer, Resource); - enum { + enum : uint32_t { TYPE_DICT = 0xFFFFFFFF, TYPE_ARRAY = 0xFFFFFFFE, }; diff --git a/core/io/packet_peer_dtls.cpp b/core/io/packet_peer_dtls.cpp index 18bef3ff3c..231c48d887 100644 --- a/core/io/packet_peer_dtls.cpp +++ b/core/io/packet_peer_dtls.cpp @@ -32,12 +32,12 @@ #include "core/config/project_settings.h" #include "core/io/file_access.h" -PacketPeerDTLS *(*PacketPeerDTLS::_create)() = nullptr; +PacketPeerDTLS *(*PacketPeerDTLS::_create)(bool p_notify_postinitialize) = nullptr; bool PacketPeerDTLS::available = false; -PacketPeerDTLS *PacketPeerDTLS::create() { +PacketPeerDTLS *PacketPeerDTLS::create(bool p_notify_postinitialize) { if (_create) { - return _create(); + return _create(p_notify_postinitialize); } return nullptr; } diff --git a/core/io/packet_peer_dtls.h b/core/io/packet_peer_dtls.h index 3990a851f7..03d97a5903 100644 --- a/core/io/packet_peer_dtls.h +++ b/core/io/packet_peer_dtls.h @@ -38,7 +38,7 @@ class PacketPeerDTLS : public PacketPeer { GDCLASS(PacketPeerDTLS, PacketPeer); protected: - static PacketPeerDTLS *(*_create)(); + static PacketPeerDTLS *(*_create)(bool p_notify_postinitialize); static void _bind_methods(); static bool available; @@ -57,7 +57,7 @@ public: virtual void disconnect_from_peer() = 0; virtual Status get_status() const = 0; - static PacketPeerDTLS *create(); + static PacketPeerDTLS *create(bool p_notify_postinitialize = true); static bool is_available(); PacketPeerDTLS() {} diff --git a/core/io/resource.cpp b/core/io/resource.cpp index 432adb88da..ff12dc5851 100644 --- a/core/io/resource.cpp +++ b/core/io/resource.cpp @@ -60,32 +60,32 @@ void Resource::set_path(const String &p_path, bool p_take_over) { p_take_over = false; // Can't take over an empty path } - ResourceCache::lock.lock(); + { + MutexLock lock(ResourceCache::lock); - if (!path_cache.is_empty()) { - ResourceCache::resources.erase(path_cache); - } + if (!path_cache.is_empty()) { + ResourceCache::resources.erase(path_cache); + } - path_cache = ""; + path_cache = ""; - Ref<Resource> existing = ResourceCache::get_ref(p_path); + Ref<Resource> existing = ResourceCache::get_ref(p_path); - if (existing.is_valid()) { - if (p_take_over) { - existing->path_cache = String(); - ResourceCache::resources.erase(p_path); - } else { - ResourceCache::lock.unlock(); - ERR_FAIL_MSG("Another resource is loaded from path '" + p_path + "' (possible cyclic resource inclusion)."); + if (existing.is_valid()) { + if (p_take_over) { + existing->path_cache = String(); + ResourceCache::resources.erase(p_path); + } else { + ERR_FAIL_MSG("Another resource is loaded from path '" + p_path + "' (possible cyclic resource inclusion)."); + } } - } - path_cache = p_path; + path_cache = p_path; - if (!path_cache.is_empty()) { - ResourceCache::resources[path_cache] = this; + if (!path_cache.is_empty()) { + ResourceCache::resources[path_cache] = this; + } } - ResourceCache::lock.unlock(); _resource_path_changed(); } @@ -416,21 +416,15 @@ void Resource::_take_over_path(const String &p_path) { } RID Resource::get_rid() const { - if (get_script_instance()) { - Callable::CallError ce; - RID ret = get_script_instance()->callp(SNAME("_get_rid"), nullptr, 0, ce); - if (ce.error == Callable::CallError::CALL_OK && ret.is_valid()) { - return ret; - } - } - if (_get_extension() && _get_extension()->get_rid) { - RID ret = RID::from_uint64(_get_extension()->get_rid(_get_extension_instance())); - if (ret.is_valid()) { - return ret; + RID ret; + if (!GDVIRTUAL_CALL(_get_rid, ret)) { +#ifndef DISABLE_DEPRECATED + if (_get_extension() && _get_extension()->get_rid) { + ret = RID::from_uint64(_get_extension()->get_rid(_get_extension_instance())); } +#endif } - - return RID(); + return ret; } #ifdef TOOLS_ENABLED @@ -492,15 +486,13 @@ void Resource::set_as_translation_remapped(bool p_remapped) { return; } - ResourceCache::lock.lock(); + MutexLock lock(ResourceCache::lock); if (p_remapped) { ResourceLoader::remapped_list.add(&remapped_list); } else { ResourceLoader::remapped_list.remove(&remapped_list); } - - ResourceCache::lock.unlock(); } #ifdef TOOLS_ENABLED @@ -558,11 +550,8 @@ void Resource::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::STRING, "resource_name"), "set_name", "get_name"); ADD_PROPERTY(PropertyInfo(Variant::STRING, "resource_scene_unique_id", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NONE), "set_scene_unique_id", "get_scene_unique_id"); - MethodInfo get_rid_bind("_get_rid"); - get_rid_bind.return_val.type = Variant::RID; - - ::ClassDB::add_virtual_method(get_class_static(), get_rid_bind, true, Vector<String>(), true); GDVIRTUAL_BIND(_setup_local_to_scene); + GDVIRTUAL_BIND(_get_rid); } Resource::Resource() : @@ -573,14 +562,13 @@ Resource::~Resource() { return; } - ResourceCache::lock.lock(); + MutexLock lock(ResourceCache::lock); // Only unregister from the cache if this is the actual resource listed there. // (Other resources can have the same value in `path_cache` if loaded with `CACHE_IGNORE`.) HashMap<String, Resource *>::Iterator E = ResourceCache::resources.find(path_cache); if (likely(E && E->value == this)) { ResourceCache::resources.remove(E); } - ResourceCache::lock.unlock(); } HashMap<String, Resource *> ResourceCache::resources; @@ -609,18 +597,20 @@ void ResourceCache::clear() { } bool ResourceCache::has(const String &p_path) { - lock.lock(); + Resource **res = nullptr; - Resource **res = resources.getptr(p_path); + { + MutexLock mutex_lock(lock); - if (res && (*res)->get_reference_count() == 0) { - // This resource is in the process of being deleted, ignore its existence. - (*res)->path_cache = String(); - resources.erase(p_path); - res = nullptr; - } + res = resources.getptr(p_path); - lock.unlock(); + if (res && (*res)->get_reference_count() == 0) { + // This resource is in the process of being deleted, ignore its existence. + (*res)->path_cache = String(); + resources.erase(p_path); + res = nullptr; + } + } if (!res) { return false; @@ -631,28 +621,27 @@ bool ResourceCache::has(const String &p_path) { Ref<Resource> ResourceCache::get_ref(const String &p_path) { Ref<Resource> ref; - lock.lock(); - - Resource **res = resources.getptr(p_path); + { + MutexLock mutex_lock(lock); + Resource **res = resources.getptr(p_path); - if (res) { - ref = Ref<Resource>(*res); - } + if (res) { + ref = Ref<Resource>(*res); + } - if (res && !ref.is_valid()) { - // This resource is in the process of being deleted, ignore its existence - (*res)->path_cache = String(); - resources.erase(p_path); - res = nullptr; + if (res && !ref.is_valid()) { + // This resource is in the process of being deleted, ignore its existence + (*res)->path_cache = String(); + resources.erase(p_path); + res = nullptr; + } } - lock.unlock(); - return ref; } void ResourceCache::get_cached_resources(List<Ref<Resource>> *p_resources) { - lock.lock(); + MutexLock mutex_lock(lock); LocalVector<String> to_remove; @@ -672,14 +661,9 @@ void ResourceCache::get_cached_resources(List<Ref<Resource>> *p_resources) { for (const String &E : to_remove) { resources.erase(E); } - - lock.unlock(); } int ResourceCache::get_cached_resource_count() { - lock.lock(); - int rc = resources.size(); - lock.unlock(); - - return rc; + MutexLock mutex_lock(lock); + return resources.size(); } diff --git a/core/io/resource.h b/core/io/resource.h index cc8a0d4387..2c1a431255 100644 --- a/core/io/resource.h +++ b/core/io/resource.h @@ -87,6 +87,8 @@ protected: virtual void reset_local_to_scene(); GDVIRTUAL0(_setup_local_to_scene); + GDVIRTUAL0RC(RID, _get_rid); + public: static Node *(*_get_local_scene_func)(); //used by editor static void (*_update_configuration_warning)(); //used by editor diff --git a/core/io/resource_importer.cpp b/core/io/resource_importer.cpp index 9e6f3ba314..a572dd562e 100644 --- a/core/io/resource_importer.cpp +++ b/core/io/resource_importer.cpp @@ -35,6 +35,8 @@ #include "core/os/os.h" #include "core/variant/variant_parser.h" +ResourceFormatImporterLoadOnStartup ResourceImporter::load_on_startup = nullptr; + bool ResourceFormatImporter::SortImporterByName::operator()(const Ref<ResourceImporter> &p_a, const Ref<ResourceImporter> &p_b) const { return p_a->get_importer_name() < p_b->get_importer_name(); } @@ -137,6 +139,20 @@ Error ResourceFormatImporter::_get_path_and_type(const String &p_path, PathAndTy } Ref<Resource> ResourceFormatImporter::load(const String &p_path, const String &p_original_path, Error *r_error, bool p_use_sub_threads, float *r_progress, CacheMode p_cache_mode) { +#ifdef TOOLS_ENABLED + // When loading a resource on startup, we use the load_on_startup callback, + // which executes the loading in the EditorFileSystem. It can reimport + // the resource and retry the load, allowing the resource to be loaded + // even if it is not yet imported. + if (ResourceImporter::load_on_startup != nullptr) { + return ResourceImporter::load_on_startup(this, p_path, r_error, p_use_sub_threads, r_progress, p_cache_mode); + } +#endif + + return load_internal(p_path, r_error, p_use_sub_threads, r_progress, p_cache_mode, false); +} + +Ref<Resource> ResourceFormatImporter::load_internal(const String &p_path, Error *r_error, bool p_use_sub_threads, float *r_progress, CacheMode p_cache_mode, bool p_silence_errors) { PathAndType pat; Error err = _get_path_and_type(p_path, pat); @@ -148,6 +164,13 @@ Ref<Resource> ResourceFormatImporter::load(const String &p_path, const String &p return Ref<Resource>(); } + if (p_silence_errors) { + // Note: Some importers do not create files in the .godot folder, so we need to check if the path is empty. + if (!pat.path.is_empty() && !FileAccess::exists(pat.path)) { + return Ref<Resource>(); + } + } + Ref<Resource> res = ResourceLoader::_load(pat.path, p_path, pat.type, p_cache_mode, r_error, p_use_sub_threads, r_progress); #ifdef TOOLS_ENABLED @@ -364,6 +387,23 @@ ResourceUID::ID ResourceFormatImporter::get_resource_uid(const String &p_path) c return pat.uid; } +Error ResourceFormatImporter::get_resource_import_info(const String &p_path, StringName &r_type, ResourceUID::ID &r_uid, String &r_import_group_file) const { + PathAndType pat; + Error err = _get_path_and_type(p_path, pat); + + if (err == OK) { + r_type = pat.type; + r_uid = pat.uid; + r_import_group_file = pat.group_file; + } else { + r_type = ""; + r_uid = ResourceUID::INVALID_ID; + r_import_group_file = ""; + } + + return err; +} + Variant ResourceFormatImporter::get_resource_metadata(const String &p_path) const { PathAndType pat; Error err = _get_path_and_type(p_path, pat); diff --git a/core/io/resource_importer.h b/core/io/resource_importer.h index dbd9e70d16..6ea5d0972a 100644 --- a/core/io/resource_importer.h +++ b/core/io/resource_importer.h @@ -35,6 +35,9 @@ #include "core/io/resource_saver.h" class ResourceImporter; +class ResourceFormatImporter; + +typedef Ref<Resource> (*ResourceFormatImporterLoadOnStartup)(ResourceFormatImporter *p_importer, const String &p_path, Error *r_error, bool p_use_sub_threads, float *r_progress, ResourceFormatLoader::CacheMode p_cache_mode); class ResourceFormatImporter : public ResourceFormatLoader { struct PathAndType { @@ -60,6 +63,7 @@ class ResourceFormatImporter : public ResourceFormatLoader { public: static ResourceFormatImporter *get_singleton() { return singleton; } virtual Ref<Resource> load(const String &p_path, const String &p_original_path = "", Error *r_error = nullptr, bool p_use_sub_threads = false, float *r_progress = nullptr, CacheMode p_cache_mode = CACHE_MODE_REUSE) override; + Ref<Resource> load_internal(const String &p_path, Error *r_error, bool p_use_sub_threads, float *r_progress, CacheMode p_cache_mode, bool p_silence_errors); virtual void get_recognized_extensions(List<String> *p_extensions) const override; virtual void get_recognized_extensions_for_type(const String &p_type, List<String> *p_extensions) const override; virtual bool recognize_path(const String &p_path, const String &p_for_type = String()) const override; @@ -93,6 +97,8 @@ public: String get_import_settings_hash() const; String get_import_base_path(const String &p_for_file) const; + Error get_resource_import_info(const String &p_path, StringName &r_type, ResourceUID::ID &r_uid, String &r_import_group_file) const; + ResourceFormatImporter(); }; @@ -103,6 +109,8 @@ protected: static void _bind_methods(); public: + static ResourceFormatImporterLoadOnStartup load_on_startup; + virtual String get_importer_name() const = 0; virtual String get_visible_name() const = 0; virtual void get_recognized_extensions(List<String> *p_extensions) const = 0; diff --git a/core/io/resource_loader.cpp b/core/io/resource_loader.cpp index c9ed4e27d9..7cf101b0de 100644 --- a/core/io/resource_loader.cpp +++ b/core/io/resource_loader.cpp @@ -207,34 +207,52 @@ void ResourceFormatLoader::_bind_methods() { /////////////////////////////////// +// These are used before and after a wait for a WorkerThreadPool task +// because that can lead to another load started in the same thread, +// something we must treat as a different stack for the purposes +// of tracking nesting. + +#define PREPARE_FOR_WTP_WAIT \ + int load_nesting_backup = ResourceLoader::load_nesting; \ + Vector<String> load_paths_stack_backup = ResourceLoader::load_paths_stack; \ + ResourceLoader::load_nesting = 0; \ + ResourceLoader::load_paths_stack.clear(); + +#define RESTORE_AFTER_WTP_WAIT \ + DEV_ASSERT(ResourceLoader::load_nesting == 0); \ + DEV_ASSERT(ResourceLoader::load_paths_stack.is_empty()); \ + ResourceLoader::load_nesting = load_nesting_backup; \ + ResourceLoader::load_paths_stack = load_paths_stack_backup; \ + load_paths_stack_backup.clear(); + // This should be robust enough to be called redundantly without issues. void ResourceLoader::LoadToken::clear() { - thread_load_mutex.lock(); - WorkerThreadPool::TaskID task_to_await = 0; - if (!local_path.is_empty()) { // Empty is used for the special case where the load task is not registered. - DEV_ASSERT(thread_load_tasks.has(local_path)); - ThreadLoadTask &load_task = thread_load_tasks[local_path]; - if (!load_task.awaited) { - task_to_await = load_task.task_id; - load_task.awaited = true; + { + MutexLock thread_load_lock(thread_load_mutex); + // User-facing tokens shouldn't be deleted until completely claimed. + DEV_ASSERT(user_rc == 0 && user_path.is_empty()); + + if (!local_path.is_empty()) { // Empty is used for the special case where the load task is not registered. + DEV_ASSERT(thread_load_tasks.has(local_path)); + ThreadLoadTask &load_task = thread_load_tasks[local_path]; + if (load_task.task_id && !load_task.awaited) { + task_to_await = load_task.task_id; + } + // Removing a task which is still in progress would be catastrophic. + // Tokens must be alive until the task thread function is done. + DEV_ASSERT(load_task.status == THREAD_LOAD_FAILED || load_task.status == THREAD_LOAD_LOADED); + thread_load_tasks.erase(local_path); + local_path.clear(); } - thread_load_tasks.erase(local_path); - local_path.clear(); } - if (!user_path.is_empty()) { - DEV_ASSERT(user_load_tokens.has(user_path)); - user_load_tokens.erase(user_path); - user_path.clear(); - } - - thread_load_mutex.unlock(); - // If task is unused, await it here, locally, now the token data is consistent. if (task_to_await) { + PREPARE_FOR_WTP_WAIT WorkerThreadPool::get_singleton()->wait_for_task_completion(task_to_await); + RESTORE_AFTER_WTP_WAIT } } @@ -246,7 +264,7 @@ Ref<Resource> ResourceLoader::_load(const String &p_path, const String &p_origin const String &original_path = p_original_path.is_empty() ? p_path : p_original_path; load_nesting++; if (load_paths_stack.size()) { - thread_load_mutex.lock(); + MutexLock thread_load_lock(thread_load_mutex); const String &parent_task_path = load_paths_stack.get(load_paths_stack.size() - 1); HashMap<String, ThreadLoadTask>::Iterator E = thread_load_tasks.find(parent_task_path); // Avoid double-tracking, for progress reporting, resources that boil down to a remapped path containing the real payload (e.g., imported resources). @@ -254,7 +272,6 @@ Ref<Resource> ResourceLoader::_load(const String &p_path, const String &p_origin if (E && !is_remapped_load) { E->value.sub_tasks.insert(p_original_path); } - thread_load_mutex.unlock(); } load_paths_stack.push_back(original_path); @@ -295,17 +312,17 @@ Ref<Resource> ResourceLoader::_load(const String &p_path, const String &p_origin ERR_FAIL_V_MSG(Ref<Resource>(), vformat("No loader found for resource: %s (expected type: %s)", p_path, p_type_hint)); } -void ResourceLoader::_thread_load_function(void *p_userdata) { +// This implementation must allow re-entrancy for a task that started awaiting in a deeper stack frame. +void ResourceLoader::_run_load_task(void *p_userdata) { ThreadLoadTask &load_task = *(ThreadLoadTask *)p_userdata; - thread_load_mutex.lock(); - caller_task_id = load_task.task_id; - if (cleaning_tasks) { - load_task.status = THREAD_LOAD_FAILED; - thread_load_mutex.unlock(); - return; + { + MutexLock thread_load_lock(thread_load_mutex); + if (cleaning_tasks) { + load_task.status = THREAD_LOAD_FAILED; + return; + } } - thread_load_mutex.unlock(); // Thread-safe either if it's the current thread or a brand new one. CallQueue *own_mq_override = nullptr; @@ -322,12 +339,21 @@ void ResourceLoader::_thread_load_function(void *p_userdata) { } // -- + bool xl_remapped = false; + const String &remapped_path = _path_remap(load_task.local_path, &xl_remapped); + + print_verbose("Loading resource: " + remapped_path); + Error load_err = OK; - Ref<Resource> res = _load(load_task.remapped_path, load_task.remapped_path != load_task.local_path ? load_task.local_path : String(), load_task.type_hint, load_task.cache_mode, &load_err, load_task.use_sub_threads, &load_task.progress); + Ref<Resource> res = _load(remapped_path, remapped_path != load_task.local_path ? load_task.local_path : String(), load_task.type_hint, load_task.cache_mode, &load_err, load_task.use_sub_threads, &load_task.progress); if (MessageQueue::get_singleton() != MessageQueue::get_main_singleton()) { MessageQueue::get_singleton()->flush(); } + if (res.is_null()) { + print_verbose("Failed loading resource: " + remapped_path); + } + thread_load_mutex.lock(); load_task.resource = res; @@ -356,27 +382,40 @@ void ResourceLoader::_thread_load_function(void *p_userdata) { unlock_pending = false; if (!ignoring) { - if (replacing) { - Ref<Resource> old_res = ResourceCache::get_ref(load_task.local_path); - if (old_res.is_valid() && old_res != load_task.resource) { - // If resource is already loaded, only replace its data, to avoid existing invalidating instances. - old_res->copy_from(load_task.resource); + ResourceCache::lock.lock(); // Check and operations must happen atomically. + bool pending_unlock = true; + Ref<Resource> old_res = ResourceCache::get_ref(load_task.local_path); + if (old_res.is_valid()) { + if (old_res != load_task.resource) { + // Resource can already exists at this point for two reasons: + // a) The load uses replace mode. + // b) There were more than one load in flight for the same path because of deadlock prevention. + // Either case, we want to keep the resource that was already there. + ResourceCache::lock.unlock(); + pending_unlock = false; + if (replacing) { + old_res->copy_from(load_task.resource); + } load_task.resource = old_res; } + } else { + load_task.resource->set_path(load_task.local_path); + } + if (pending_unlock) { + ResourceCache::lock.unlock(); } - load_task.resource->set_path(load_task.local_path, replacing); } else { load_task.resource->set_path_cache(load_task.local_path); } - if (load_task.xl_remapped) { + if (xl_remapped) { load_task.resource->set_as_translation_remapped(true); } #ifdef TOOLS_ENABLED load_task.resource->set_edited(false); if (timestamp_on_load) { - uint64_t mt = FileAccess::get_modified_time(load_task.remapped_path); + uint64_t mt = FileAccess::get_modified_time(remapped_path); //printf("mt %s: %lli\n",remapped_path.utf8().get_data(),mt); load_task.resource->set_last_modified_time(mt); } @@ -426,36 +465,44 @@ static String _validate_local_path(const String &p_path) { } Error ResourceLoader::load_threaded_request(const String &p_path, const String &p_type_hint, bool p_use_sub_threads, ResourceFormatLoader::CacheMode p_cache_mode) { - thread_load_mutex.lock(); - if (user_load_tokens.has(p_path)) { + Ref<ResourceLoader::LoadToken> token = _load_start(p_path, p_type_hint, p_use_sub_threads ? LOAD_THREAD_DISTRIBUTE : LOAD_THREAD_SPAWN_SINGLE, p_cache_mode, true); + return token.is_valid() ? OK : FAILED; +} + +ResourceLoader::LoadToken *ResourceLoader::_load_threaded_request_reuse_user_token(const String &p_path) { + HashMap<String, LoadToken *>::Iterator E = user_load_tokens.find(p_path); + if (E) { print_verbose("load_threaded_request(): Another threaded load for resource path '" + p_path + "' has been initiated. Not an error."); - user_load_tokens[p_path]->reference(); // Additional request. - thread_load_mutex.unlock(); - return OK; - } - user_load_tokens[p_path] = nullptr; - thread_load_mutex.unlock(); - - Ref<ResourceLoader::LoadToken> token = _load_start(p_path, p_type_hint, p_use_sub_threads ? LOAD_THREAD_DISTRIBUTE : LOAD_THREAD_SPAWN_SINGLE, p_cache_mode); - if (token.is_valid()) { - thread_load_mutex.lock(); - token->user_path = p_path; - token->reference(); // First request. - user_load_tokens[p_path] = token.ptr(); - print_lt("REQUEST: user load tokens: " + itos(user_load_tokens.size())); - thread_load_mutex.unlock(); - return OK; + LoadToken *token = E->value; + token->user_rc++; + return token; } else { - return FAILED; + return nullptr; } } +void ResourceLoader::_load_threaded_request_setup_user_token(LoadToken *p_token, const String &p_path) { + p_token->user_path = p_path; + p_token->reference(); // Extra RC until all user requests have been gotten. + p_token->user_rc = 1; + user_load_tokens[p_path] = p_token; + print_lt("REQUEST: user load tokens: " + itos(user_load_tokens.size())); +} + Ref<Resource> ResourceLoader::load(const String &p_path, const String &p_type_hint, ResourceFormatLoader::CacheMode p_cache_mode, Error *r_error) { if (r_error) { *r_error = OK; } - Ref<LoadToken> load_token = _load_start(p_path, p_type_hint, LOAD_THREAD_FROM_CURRENT, p_cache_mode); + LoadThreadMode thread_mode = LOAD_THREAD_FROM_CURRENT; + if (WorkerThreadPool::get_singleton()->get_caller_task_id() != WorkerThreadPool::INVALID_TASK_ID) { + // If user is initiating a single-threaded load from a WorkerThreadPool task, + // we instead spawn a new task so there's a precondition that a load in a pool task + // is always initiated by the engine. That makes certain aspects simpler, such as + // cyclic load detection and awaiting. + thread_mode = LOAD_THREAD_SPAWN_SINGLE; + } + Ref<LoadToken> load_token = _load_start(p_path, p_type_hint, thread_mode, p_cache_mode); if (!load_token.is_valid()) { if (r_error) { *r_error = FAILED; @@ -467,7 +514,7 @@ Ref<Resource> ResourceLoader::load(const String &p_path, const String &p_type_hi return res; } -Ref<ResourceLoader::LoadToken> ResourceLoader::_load_start(const String &p_path, const String &p_type_hint, LoadThreadMode p_thread_mode, ResourceFormatLoader::CacheMode p_cache_mode) { +Ref<ResourceLoader::LoadToken> ResourceLoader::_load_start(const String &p_path, const String &p_type_hint, LoadThreadMode p_thread_mode, ResourceFormatLoader::CacheMode p_cache_mode, bool p_for_user) { String local_path = _validate_local_path(p_path); bool ignoring_cache = p_cache_mode == ResourceFormatLoader::CACHE_MODE_IGNORE || p_cache_mode == ResourceFormatLoader::CACHE_MODE_IGNORE_DEEP; @@ -480,9 +527,21 @@ Ref<ResourceLoader::LoadToken> ResourceLoader::_load_start(const String &p_path, { MutexLock thread_load_lock(thread_load_mutex); + if (p_for_user) { + LoadToken *existing_token = _load_threaded_request_reuse_user_token(p_path); + if (existing_token) { + return Ref<LoadToken>(existing_token); + } + } + if (!ignoring_cache && thread_load_tasks.has(local_path)) { load_token = Ref<LoadToken>(thread_load_tasks[local_path].load_token); if (load_token.is_valid()) { + if (p_for_user) { + // Load task exists, with no user tokens at the moment. + // Let's "attach" to it. + _load_threaded_request_setup_user_token(load_token.ptr(), p_path); + } return load_token; } else { // The token is dying (reached 0 on another thread). @@ -493,12 +552,14 @@ Ref<ResourceLoader::LoadToken> ResourceLoader::_load_start(const String &p_path, load_token.instantiate(); load_token->local_path = local_path; + if (p_for_user) { + _load_threaded_request_setup_user_token(load_token.ptr(), p_path); + } //create load task { ThreadLoadTask load_task; - load_task.remapped_path = _path_remap(local_path, &load_task.xl_remapped); load_task.load_token = load_token.ptr(); load_task.local_path = local_path; load_task.type_hint = p_type_hint; @@ -511,6 +572,7 @@ Ref<ResourceLoader::LoadToken> ResourceLoader::_load_start(const String &p_path, load_task.resource = existing; load_task.status = THREAD_LOAD_LOADED; load_task.progress = 1.0; + DEV_ASSERT(!thread_load_tasks.has(local_path)); thread_load_tasks[local_path] = load_task; return load_token; } @@ -532,14 +594,20 @@ Ref<ResourceLoader::LoadToken> ResourceLoader::_load_start(const String &p_path, run_on_current_thread = must_not_register || p_thread_mode == LOAD_THREAD_FROM_CURRENT; if (run_on_current_thread) { - load_task_ptr->thread_id = Thread::get_caller_id(); + // The current thread may happen to be a thread from the pool. + WorkerThreadPool::TaskID tid = WorkerThreadPool::get_singleton()->get_caller_task_id(); + if (tid != WorkerThreadPool::INVALID_TASK_ID) { + load_task_ptr->task_id = tid; + } else { + load_task_ptr->thread_id = Thread::get_caller_id(); + } } else { - load_task_ptr->task_id = WorkerThreadPool::get_singleton()->add_native_task(&ResourceLoader::_thread_load_function, load_task_ptr); + load_task_ptr->task_id = WorkerThreadPool::get_singleton()->add_native_task(&ResourceLoader::_run_load_task, load_task_ptr); } - } + } // MutexLock(thread_load_mutex). if (run_on_current_thread) { - _thread_load_function(load_task_ptr); + _run_load_task(load_task_ptr); if (must_not_register) { load_token->res_if_unregistered = load_task_ptr->resource; } @@ -626,22 +694,16 @@ Ref<Resource> ResourceLoader::load_threaded_get(const String &p_path, Error *r_e } LoadToken *load_token = user_load_tokens[p_path]; - if (!load_token) { - // This happens if requested from one thread and rapidly querying from another. - if (r_error) { - *r_error = ERR_BUSY; - } - return Ref<Resource>(); - } + DEV_ASSERT(load_token->user_rc >= 1); // Support userland requesting on the main thread before the load is reported to be complete. if (Thread::is_main_thread() && !load_token->local_path.is_empty()) { const ThreadLoadTask &load_task = thread_load_tasks[load_token->local_path]; while (load_task.status == THREAD_LOAD_IN_PROGRESS) { - thread_load_lock.~MutexLock(); + thread_load_lock.temp_unlock(); bool exit = !_ensure_load_progress(); OS::get_singleton()->delay_usec(1000); - new (&thread_load_lock) MutexLock(thread_load_mutex); + thread_load_lock.temp_relock(); if (exit) { break; } @@ -649,8 +711,15 @@ Ref<Resource> ResourceLoader::load_threaded_get(const String &p_path, Error *r_e } res = _load_complete_inner(*load_token, r_error, thread_load_lock); - if (load_token->unreference()) { - memdelete(load_token); + + load_token->user_rc--; + if (load_token->user_rc == 0) { + load_token->user_path.clear(); + user_load_tokens.erase(p_path); + if (load_token->unreference()) { + memdelete(load_token); + load_token = nullptr; + } } } @@ -682,7 +751,7 @@ Ref<Resource> ResourceLoader::_load_complete_inner(LoadToken &p_load_token, Erro if (load_task.status == THREAD_LOAD_IN_PROGRESS) { DEV_ASSERT((load_task.task_id == 0) != (load_task.thread_id == 0)); - if ((load_task.task_id != 0 && load_task.task_id == caller_task_id) || + if ((load_task.task_id != 0 && load_task.task_id == WorkerThreadPool::get_singleton()->get_caller_task_id()) || (load_task.thread_id != 0 && load_task.thread_id == Thread::get_caller_id())) { // Load is in progress, but it's precisely this thread the one in charge. // That means this is a cyclic load. @@ -693,55 +762,45 @@ Ref<Resource> ResourceLoader::_load_complete_inner(LoadToken &p_load_token, Erro } bool loader_is_wtp = load_task.task_id != 0; - Error wtp_task_err = FAILED; if (loader_is_wtp) { // Loading thread is in the worker pool. + p_thread_load_lock.temp_unlock(); + + PREPARE_FOR_WTP_WAIT + Error wait_err = WorkerThreadPool::get_singleton()->wait_for_task_completion(load_task.task_id); + RESTORE_AFTER_WTP_WAIT + + DEV_ASSERT(!wait_err || wait_err == ERR_BUSY); + if (wait_err == ERR_BUSY) { + // The WorkerThreadPool has reported that the current task wants to await on an older one. + // That't not allowed for safety, to avoid deadlocks. Fortunately, though, in the context of + // resource loading that means that the task to wait for can be restarted here to break the + // cycle, with as much recursion into this process as needed. + // When the stack is eventually unrolled, the original load will have been notified to go on. + _run_load_task(&load_task); + } + + p_thread_load_lock.temp_relock(); load_task.awaited = true; - thread_load_mutex.unlock(); - wtp_task_err = WorkerThreadPool::get_singleton()->wait_for_task_completion(load_task.task_id); - } - if (load_task.status == THREAD_LOAD_IN_PROGRESS) { // If early errored, awaiting would deadlock. - if (loader_is_wtp) { - if (wtp_task_err == ERR_BUSY) { - // The WorkerThreadPool has reported that the current task wants to await on an older one. - // That't not allowed for safety, to avoid deadlocks. Fortunately, though, in the context of - // resource loading that means that the task to wait for can be restarted here to break the - // cycle, with as much recursion into this process as needed. - // When the stack is eventually unrolled, the original load will have been notified to go on. - // CACHE_MODE_IGNORE is needed because, otherwise, the new request would just see there's - // an ongoing load for that resource and wait for it again. This value forces a new load. - Ref<ResourceLoader::LoadToken> token = _load_start(load_task.local_path, load_task.type_hint, LOAD_THREAD_DISTRIBUTE, ResourceFormatLoader::CACHE_MODE_IGNORE); - Ref<Resource> resource = _load_complete(*token.ptr(), &wtp_task_err); - if (r_error) { - *r_error = wtp_task_err; - } - thread_load_mutex.lock(); - return resource; - } else { - DEV_ASSERT(wtp_task_err == OK); - thread_load_mutex.lock(); - } - } else if (load_task.need_wait) { - // Loading thread is main or user thread. - if (!load_task.cond_var) { - load_task.cond_var = memnew(ConditionVariable); - } - load_task.awaiters_count++; - do { - load_task.cond_var->wait(p_thread_load_lock); - DEV_ASSERT(thread_load_tasks.has(p_load_token.local_path) && p_load_token.get_reference_count()); - } while (load_task.need_wait); - load_task.awaiters_count--; - if (load_task.awaiters_count == 0) { - memdelete(load_task.cond_var); - load_task.cond_var = nullptr; - } + DEV_ASSERT(load_task.status == THREAD_LOAD_FAILED || load_task.status == THREAD_LOAD_LOADED); + } else if (load_task.need_wait) { + // Loading thread is main or user thread. + if (!load_task.cond_var) { + load_task.cond_var = memnew(ConditionVariable); } - } else { - if (loader_is_wtp) { - thread_load_mutex.lock(); + load_task.awaiters_count++; + do { + load_task.cond_var->wait(p_thread_load_lock); + DEV_ASSERT(thread_load_tasks.has(p_load_token.local_path) && p_load_token.get_reference_count()); + } while (load_task.need_wait); + load_task.awaiters_count--; + if (load_task.awaiters_count == 0) { + memdelete(load_task.cond_var); + load_task.cond_var = nullptr; } + + DEV_ASSERT(load_task.status == THREAD_LOAD_FAILED || load_task.status == THREAD_LOAD_LOADED); } } @@ -1055,36 +1114,39 @@ String ResourceLoader::_path_remap(const String &p_path, bool *r_translation_rem new_path = path_remaps[new_path]; } else { // Try file remap. - Error err; - Ref<FileAccess> f = FileAccess::open(new_path + ".remap", FileAccess::READ, &err); - if (f.is_valid()) { - VariantParser::StreamFile stream; - stream.f = f; - - String assign; - Variant value; - VariantParser::Tag next_tag; - - int lines = 0; - String error_text; - while (true) { - assign = Variant(); - next_tag.fields.clear(); - next_tag.name = String(); - - err = VariantParser::parse_tag_assign_eof(&stream, lines, error_text, next_tag, assign, value, nullptr, true); - if (err == ERR_FILE_EOF) { - break; - } else if (err != OK) { - ERR_PRINT("Parse error: " + p_path + ".remap:" + itos(lines) + " error: " + error_text + "."); - break; - } + // Usually, there's no remap file and FileAccess::exists() is faster than FileAccess::open(). + if (FileAccess::exists(new_path + ".remap")) { + Error err; + Ref<FileAccess> f = FileAccess::open(new_path + ".remap", FileAccess::READ, &err); + if (f.is_valid()) { + VariantParser::StreamFile stream; + stream.f = f; + + String assign; + Variant value; + VariantParser::Tag next_tag; + + int lines = 0; + String error_text; + while (true) { + assign = Variant(); + next_tag.fields.clear(); + next_tag.name = String(); + + err = VariantParser::parse_tag_assign_eof(&stream, lines, error_text, next_tag, assign, value, nullptr, true); + if (err == ERR_FILE_EOF) { + break; + } else if (err != OK) { + ERR_PRINT("Parse error: " + p_path + ".remap:" + itos(lines) + " error: " + error_text + "."); + break; + } - if (assign == "path") { - new_path = value; - break; - } else if (next_tag.name != "remap") { - break; + if (assign == "path") { + new_path = value; + break; + } else if (next_tag.name != "remap") { + break; + } } } } @@ -1106,17 +1168,17 @@ String ResourceLoader::path_remap(const String &p_path) { } void ResourceLoader::reload_translation_remaps() { - ResourceCache::lock.lock(); - List<Resource *> to_reload; - SelfList<Resource> *E = remapped_list.first(); - while (E) { - to_reload.push_back(E->self()); - E = E->next(); - } + { + MutexLock lock(ResourceCache::lock); + SelfList<Resource> *E = remapped_list.first(); - ResourceCache::lock.unlock(); + while (E) { + to_reload.push_back(E->self()); + E = E->next(); + } + } //now just make sure to not delete any of these resources while changing locale.. while (to_reload.front()) { @@ -1156,7 +1218,7 @@ void ResourceLoader::clear_translation_remaps() { void ResourceLoader::clear_thread_load_tasks() { // Bring the thing down as quickly as possible without causing deadlocks or leaks. - thread_load_mutex.lock(); + MutexLock thread_load_lock(thread_load_mutex); cleaning_tasks = true; while (true) { @@ -1175,21 +1237,23 @@ void ResourceLoader::clear_thread_load_tasks() { if (none_running) { break; } - thread_load_mutex.unlock(); + thread_load_lock.temp_unlock(); OS::get_singleton()->delay_usec(1000); - thread_load_mutex.lock(); + thread_load_lock.temp_relock(); } while (user_load_tokens.begin()) { - // User load tokens remove themselves from the map on destruction. - memdelete(user_load_tokens.begin()->value); + LoadToken *user_token = user_load_tokens.begin()->value; + user_load_tokens.remove(user_load_tokens.begin()); + DEV_ASSERT(user_token->user_rc > 0 && !user_token->user_path.is_empty()); + user_token->user_path.clear(); + user_token->user_rc = 0; + user_token->unreference(); } - user_load_tokens.clear(); thread_load_tasks.clear(); cleaning_tasks = false; - thread_load_mutex.unlock(); } void ResourceLoader::load_path_remaps() { @@ -1302,12 +1366,15 @@ bool ResourceLoader::abort_on_missing_resource = true; bool ResourceLoader::timestamp_on_load = false; thread_local int ResourceLoader::load_nesting = 0; -thread_local WorkerThreadPool::TaskID ResourceLoader::caller_task_id = 0; thread_local Vector<String> ResourceLoader::load_paths_stack; thread_local HashMap<int, HashMap<String, Ref<Resource>>> ResourceLoader::res_ref_overrides; +SafeBinaryMutex<ResourceLoader::BINARY_MUTEX_TAG> &_get_res_loader_mutex() { + return ResourceLoader::thread_load_mutex; +} + template <> -thread_local uint32_t SafeBinaryMutex<ResourceLoader::BINARY_MUTEX_TAG>::count = 0; +thread_local SafeBinaryMutex<ResourceLoader::BINARY_MUTEX_TAG>::TLSData SafeBinaryMutex<ResourceLoader::BINARY_MUTEX_TAG>::tls_data(_get_res_loader_mutex()); SafeBinaryMutex<ResourceLoader::BINARY_MUTEX_TAG> ResourceLoader::thread_load_mutex; HashMap<String, ResourceLoader::ThreadLoadTask> ResourceLoader::thread_load_tasks; bool ResourceLoader::cleaning_tasks = false; diff --git a/core/io/resource_loader.h b/core/io/resource_loader.h index ec9997891e..f75bf019fb 100644 --- a/core/io/resource_loader.h +++ b/core/io/resource_loader.h @@ -100,6 +100,8 @@ typedef Error (*ResourceLoaderImport)(const String &p_path); typedef void (*ResourceLoadedCallback)(Ref<Resource> p_resource, const String &p_path); class ResourceLoader { + friend class LoadToken; + enum { MAX_LOADERS = 64 }; @@ -121,6 +123,7 @@ public: struct LoadToken : public RefCounted { String local_path; String user_path; + uint32_t user_rc = 0; // Having user RC implies regular RC incremented in one, until the user RC reaches zero. Ref<Resource> res_if_unregistered; void clear(); @@ -130,10 +133,13 @@ public: static const int BINARY_MUTEX_TAG = 1; - static Ref<LoadToken> _load_start(const String &p_path, const String &p_type_hint, LoadThreadMode p_thread_mode, ResourceFormatLoader::CacheMode p_cache_mode); + static Ref<LoadToken> _load_start(const String &p_path, const String &p_type_hint, LoadThreadMode p_thread_mode, ResourceFormatLoader::CacheMode p_cache_mode, bool p_for_user = false); static Ref<Resource> _load_complete(LoadToken &p_load_token, Error *r_error); private: + static LoadToken *_load_threaded_request_reuse_user_token(const String &p_path); + static void _load_threaded_request_setup_user_token(LoadToken *p_token, const String &p_path); + static Ref<Resource> _load_complete_inner(LoadToken &p_load_token, Error *r_error, MutexLock<SafeBinaryMutex<BINARY_MUTEX_TAG>> &p_thread_load_lock); static Ref<ResourceFormatLoader> loader[MAX_LOADERS]; @@ -171,7 +177,6 @@ private: bool need_wait = true; LoadToken *load_token = nullptr; String local_path; - String remapped_path; String type_hint; float progress = 0.0f; float max_reported_progress = 0.0f; @@ -180,18 +185,19 @@ private: ResourceFormatLoader::CacheMode cache_mode = ResourceFormatLoader::CACHE_MODE_REUSE; Error error = OK; Ref<Resource> resource; - bool xl_remapped = false; bool use_sub_threads = false; HashSet<String> sub_tasks; }; - static void _thread_load_function(void *p_userdata); + static void _run_load_task(void *p_userdata); static thread_local int load_nesting; - static thread_local WorkerThreadPool::TaskID caller_task_id; static thread_local HashMap<int, HashMap<String, Ref<Resource>>> res_ref_overrides; // Outermost key is nesting level. static thread_local Vector<String> load_paths_stack; + static SafeBinaryMutex<BINARY_MUTEX_TAG> thread_load_mutex; + friend SafeBinaryMutex<BINARY_MUTEX_TAG> &_get_res_loader_mutex(); + static HashMap<String, ThreadLoadTask> thread_load_tasks; static bool cleaning_tasks; diff --git a/core/io/stream_peer_tls.cpp b/core/io/stream_peer_tls.cpp index 69877974e6..f04e217a26 100644 --- a/core/io/stream_peer_tls.cpp +++ b/core/io/stream_peer_tls.cpp @@ -32,11 +32,11 @@ #include "core/config/engine.h" -StreamPeerTLS *(*StreamPeerTLS::_create)() = nullptr; +StreamPeerTLS *(*StreamPeerTLS::_create)(bool p_notify_postinitialize) = nullptr; -StreamPeerTLS *StreamPeerTLS::create() { +StreamPeerTLS *StreamPeerTLS::create(bool p_notify_postinitialize) { if (_create) { - return _create(); + return _create(p_notify_postinitialize); } return nullptr; } diff --git a/core/io/stream_peer_tls.h b/core/io/stream_peer_tls.h index 5894abb7a4..3e03e25a2d 100644 --- a/core/io/stream_peer_tls.h +++ b/core/io/stream_peer_tls.h @@ -38,7 +38,7 @@ class StreamPeerTLS : public StreamPeer { GDCLASS(StreamPeerTLS, StreamPeer); protected: - static StreamPeerTLS *(*_create)(); + static StreamPeerTLS *(*_create)(bool p_notify_postinitialize); static void _bind_methods(); public: @@ -58,7 +58,7 @@ public: virtual void disconnect_from_stream() = 0; - static StreamPeerTLS *create(); + static StreamPeerTLS *create(bool p_notify_postinitialize = true); static bool is_available(); diff --git a/core/math/a_star.cpp b/core/math/a_star.cpp index 4497604947..c53fd3d330 100644 --- a/core/math/a_star.cpp +++ b/core/math/a_star.cpp @@ -391,9 +391,9 @@ bool AStar3D::_solve(Point *begin_point, Point *end_point) { return found_route; } -real_t AStar3D::_estimate_cost(int64_t p_from_id, int64_t p_to_id) { +real_t AStar3D::_estimate_cost(int64_t p_from_id, int64_t p_end_id) { real_t scost; - if (GDVIRTUAL_CALL(_estimate_cost, p_from_id, p_to_id, scost)) { + if (GDVIRTUAL_CALL(_estimate_cost, p_from_id, p_end_id, scost)) { return scost; } @@ -401,11 +401,11 @@ real_t AStar3D::_estimate_cost(int64_t p_from_id, int64_t p_to_id) { bool from_exists = points.lookup(p_from_id, from_point); ERR_FAIL_COND_V_MSG(!from_exists, 0, vformat("Can't estimate cost. Point with id: %d doesn't exist.", p_from_id)); - Point *to_point = nullptr; - bool to_exists = points.lookup(p_to_id, to_point); - ERR_FAIL_COND_V_MSG(!to_exists, 0, vformat("Can't estimate cost. Point with id: %d doesn't exist.", p_to_id)); + Point *end_point = nullptr; + bool end_exists = points.lookup(p_end_id, end_point); + ERR_FAIL_COND_V_MSG(!end_exists, 0, vformat("Can't estimate cost. Point with id: %d doesn't exist.", p_end_id)); - return from_point->pos.distance_to(to_point->pos); + return from_point->pos.distance_to(end_point->pos); } real_t AStar3D::_compute_cost(int64_t p_from_id, int64_t p_to_id) { @@ -579,7 +579,7 @@ void AStar3D::_bind_methods() { ClassDB::bind_method(D_METHOD("get_point_path", "from_id", "to_id", "allow_partial_path"), &AStar3D::get_point_path, DEFVAL(false)); ClassDB::bind_method(D_METHOD("get_id_path", "from_id", "to_id", "allow_partial_path"), &AStar3D::get_id_path, DEFVAL(false)); - GDVIRTUAL_BIND(_estimate_cost, "from_id", "to_id") + GDVIRTUAL_BIND(_estimate_cost, "from_id", "end_id") GDVIRTUAL_BIND(_compute_cost, "from_id", "to_id") } @@ -675,9 +675,9 @@ Vector2 AStar2D::get_closest_position_in_segment(const Vector2 &p_point) const { return Vector2(p.x, p.y); } -real_t AStar2D::_estimate_cost(int64_t p_from_id, int64_t p_to_id) { +real_t AStar2D::_estimate_cost(int64_t p_from_id, int64_t p_end_id) { real_t scost; - if (GDVIRTUAL_CALL(_estimate_cost, p_from_id, p_to_id, scost)) { + if (GDVIRTUAL_CALL(_estimate_cost, p_from_id, p_end_id, scost)) { return scost; } @@ -685,11 +685,11 @@ real_t AStar2D::_estimate_cost(int64_t p_from_id, int64_t p_to_id) { bool from_exists = astar.points.lookup(p_from_id, from_point); ERR_FAIL_COND_V_MSG(!from_exists, 0, vformat("Can't estimate cost. Point with id: %d doesn't exist.", p_from_id)); - AStar3D::Point *to_point = nullptr; - bool to_exists = astar.points.lookup(p_to_id, to_point); - ERR_FAIL_COND_V_MSG(!to_exists, 0, vformat("Can't estimate cost. Point with id: %d doesn't exist.", p_to_id)); + AStar3D::Point *end_point = nullptr; + bool to_exists = astar.points.lookup(p_end_id, end_point); + ERR_FAIL_COND_V_MSG(!to_exists, 0, vformat("Can't estimate cost. Point with id: %d doesn't exist.", p_end_id)); - return from_point->pos.distance_to(to_point->pos); + return from_point->pos.distance_to(end_point->pos); } real_t AStar2D::_compute_cost(int64_t p_from_id, int64_t p_to_id) { @@ -918,6 +918,6 @@ void AStar2D::_bind_methods() { ClassDB::bind_method(D_METHOD("get_point_path", "from_id", "to_id", "allow_partial_path"), &AStar2D::get_point_path, DEFVAL(false)); ClassDB::bind_method(D_METHOD("get_id_path", "from_id", "to_id", "allow_partial_path"), &AStar2D::get_id_path, DEFVAL(false)); - GDVIRTUAL_BIND(_estimate_cost, "from_id", "to_id") + GDVIRTUAL_BIND(_estimate_cost, "from_id", "end_id") GDVIRTUAL_BIND(_compute_cost, "from_id", "to_id") } diff --git a/core/math/a_star.h b/core/math/a_star.h index 8e054c4789..143a3bec61 100644 --- a/core/math/a_star.h +++ b/core/math/a_star.h @@ -120,7 +120,7 @@ class AStar3D : public RefCounted { protected: static void _bind_methods(); - virtual real_t _estimate_cost(int64_t p_from_id, int64_t p_to_id); + virtual real_t _estimate_cost(int64_t p_from_id, int64_t p_end_id); virtual real_t _compute_cost(int64_t p_from_id, int64_t p_to_id); GDVIRTUAL2RC(real_t, _estimate_cost, int64_t, int64_t) @@ -176,7 +176,7 @@ class AStar2D : public RefCounted { protected: static void _bind_methods(); - virtual real_t _estimate_cost(int64_t p_from_id, int64_t p_to_id); + virtual real_t _estimate_cost(int64_t p_from_id, int64_t p_end_id); virtual real_t _compute_cost(int64_t p_from_id, int64_t p_to_id); GDVIRTUAL2RC(real_t, _estimate_cost, int64_t, int64_t) diff --git a/core/math/a_star_grid_2d.cpp b/core/math/a_star_grid_2d.cpp index 984bb1c9c1..b1d2f82f9d 100644 --- a/core/math/a_star_grid_2d.cpp +++ b/core/math/a_star_grid_2d.cpp @@ -535,12 +535,12 @@ bool AStarGrid2D::_solve(Point *p_begin_point, Point *p_end_point) { return found_route; } -real_t AStarGrid2D::_estimate_cost(const Vector2i &p_from_id, const Vector2i &p_to_id) { +real_t AStarGrid2D::_estimate_cost(const Vector2i &p_from_id, const Vector2i &p_end_id) { real_t scost; - if (GDVIRTUAL_CALL(_estimate_cost, p_from_id, p_to_id, scost)) { + if (GDVIRTUAL_CALL(_estimate_cost, p_from_id, p_end_id, scost)) { return scost; } - return heuristics[default_estimate_heuristic](p_from_id, p_to_id); + return heuristics[default_estimate_heuristic](p_from_id, p_end_id); } real_t AStarGrid2D::_compute_cost(const Vector2i &p_from_id, const Vector2i &p_to_id) { @@ -562,6 +562,33 @@ Vector2 AStarGrid2D::get_point_position(const Vector2i &p_id) const { return _get_point_unchecked(p_id)->pos; } +TypedArray<Dictionary> AStarGrid2D::get_point_data_in_region(const Rect2i &p_region) const { + ERR_FAIL_COND_V_MSG(dirty, TypedArray<Dictionary>(), "Grid is not initialized. Call the update method."); + const Rect2i inter_region = region.intersection(p_region); + + const int32_t start_x = inter_region.position.x - region.position.x; + const int32_t start_y = inter_region.position.y - region.position.y; + const int32_t end_x = inter_region.get_end().x - region.position.x; + const int32_t end_y = inter_region.get_end().y - region.position.y; + + TypedArray<Dictionary> data; + + for (int32_t y = start_y; y < end_y; y++) { + for (int32_t x = start_x; x < end_x; x++) { + const Point &p = points[y][x]; + + Dictionary dict; + dict["id"] = p.id; + dict["position"] = p.pos; + dict["solid"] = p.solid; + dict["weight_scale"] = p.weight_scale; + data.push_back(dict); + } + } + + return data; +} + Vector<Vector2> AStarGrid2D::get_point_path(const Vector2i &p_from_id, const Vector2i &p_to_id, bool p_allow_partial_path) { ERR_FAIL_COND_V_MSG(dirty, Vector<Vector2>(), "Grid is not initialized. Call the update method."); ERR_FAIL_COND_V_MSG(!is_in_boundsv(p_from_id), Vector<Vector2>(), vformat("Can't get id path. Point %s out of bounds %s.", p_from_id, region)); @@ -698,10 +725,11 @@ void AStarGrid2D::_bind_methods() { ClassDB::bind_method(D_METHOD("clear"), &AStarGrid2D::clear); ClassDB::bind_method(D_METHOD("get_point_position", "id"), &AStarGrid2D::get_point_position); + ClassDB::bind_method(D_METHOD("get_point_data_in_region", "region"), &AStarGrid2D::get_point_data_in_region); ClassDB::bind_method(D_METHOD("get_point_path", "from_id", "to_id", "allow_partial_path"), &AStarGrid2D::get_point_path, DEFVAL(false)); ClassDB::bind_method(D_METHOD("get_id_path", "from_id", "to_id", "allow_partial_path"), &AStarGrid2D::get_id_path, DEFVAL(false)); - GDVIRTUAL_BIND(_estimate_cost, "from_id", "to_id") + GDVIRTUAL_BIND(_estimate_cost, "from_id", "end_id") GDVIRTUAL_BIND(_compute_cost, "from_id", "to_id") ADD_PROPERTY(PropertyInfo(Variant::RECT2I, "region"), "set_region", "get_region"); diff --git a/core/math/a_star_grid_2d.h b/core/math/a_star_grid_2d.h index 1a9f6dcc11..c2be0bbf29 100644 --- a/core/math/a_star_grid_2d.h +++ b/core/math/a_star_grid_2d.h @@ -151,7 +151,7 @@ private: // Internal routines. protected: static void _bind_methods(); - virtual real_t _estimate_cost(const Vector2i &p_from_id, const Vector2i &p_to_id); + virtual real_t _estimate_cost(const Vector2i &p_from_id, const Vector2i &p_end_id); virtual real_t _compute_cost(const Vector2i &p_from_id, const Vector2i &p_to_id); GDVIRTUAL2RC(real_t, _estimate_cost, Vector2i, Vector2i) @@ -209,6 +209,7 @@ public: void clear(); Vector2 get_point_position(const Vector2i &p_id) const; + TypedArray<Dictionary> get_point_data_in_region(const Rect2i &p_region) const; Vector<Vector2> get_point_path(const Vector2i &p_from, const Vector2i &p_to, bool p_allow_partial_path = false); TypedArray<Vector2i> get_id_path(const Vector2i &p_from, const Vector2i &p_to, bool p_allow_partial_path = false); }; diff --git a/core/math/math_funcs.h b/core/math/math_funcs.h index fd53ed28fd..1afc5f4bbb 100644 --- a/core/math/math_funcs.h +++ b/core/math/math_funcs.h @@ -105,6 +105,9 @@ public: static _ALWAYS_INLINE_ double fmod(double p_x, double p_y) { return ::fmod(p_x, p_y); } static _ALWAYS_INLINE_ float fmod(float p_x, float p_y) { return ::fmodf(p_x, p_y); } + static _ALWAYS_INLINE_ double modf(double p_x, double *r_y) { return ::modf(p_x, r_y); } + static _ALWAYS_INLINE_ float modf(float p_x, float *r_y) { return ::modff(p_x, r_y); } + static _ALWAYS_INLINE_ double floor(double p_x) { return ::floor(p_x); } static _ALWAYS_INLINE_ float floor(float p_x) { return ::floorf(p_x); } diff --git a/core/math/random_pcg.cpp b/core/math/random_pcg.cpp index 55787a0b57..c286a60421 100644 --- a/core/math/random_pcg.cpp +++ b/core/math/random_pcg.cpp @@ -60,6 +60,11 @@ int64_t RandomPCG::rand_weighted(const Vector<float> &p_weights) { } } + for (int64_t i = weights_size - 1; i >= 0; --i) { + if (weights[i] > 0) { + return i; + } + } return -1; } diff --git a/core/object/class_db.cpp b/core/object/class_db.cpp index 7d58f7a724..a65411629f 100644 --- a/core/object/class_db.cpp +++ b/core/object/class_db.cpp @@ -181,7 +181,7 @@ public: return 0; } - static GDExtensionObjectPtr placeholder_class_create_instance(void *p_class_userdata) { + static GDExtensionObjectPtr placeholder_class_create_instance(void *p_class_userdata, GDExtensionBool p_notify_postinitialize) { ClassDB::ClassInfo *ti = (ClassDB::ClassInfo *)p_class_userdata; // Find the closest native parent, that isn't a runtime class. @@ -192,7 +192,7 @@ public: ERR_FAIL_NULL_V(native_parent->creation_func, nullptr); // Construct a placeholder. - Object *obj = native_parent->creation_func(); + Object *obj = native_parent->creation_func(static_cast<bool>(p_notify_postinitialize)); // ClassDB::set_object_extension_instance() won't be called for placeholders. // We need need to make sure that all the things it would have done (even if @@ -271,6 +271,22 @@ void ClassDB::get_extensions_class_list(List<StringName> *p_classes) { p_classes->sort_custom<StringName::AlphCompare>(); } + +void ClassDB::get_extension_class_list(const Ref<GDExtension> &p_extension, List<StringName> *p_classes) { + OBJTYPE_RLOCK; + + for (const KeyValue<StringName, ClassInfo> &E : classes) { + if (E.value.api != API_EXTENSION && E.value.api != API_EDITOR_EXTENSION) { + continue; + } + if (!E.value.gdextension || E.value.gdextension->library != p_extension.ptr()) { + continue; + } + p_classes->push_back(E.key); + } + + p_classes->sort_custom<StringName::AlphCompare>(); +} #endif void ClassDB::get_inheriters_from_class(const StringName &p_class, List<StringName> *p_classes) { @@ -525,12 +541,12 @@ StringName ClassDB::get_compatibility_class(const StringName &p_class) { return StringName(); } -Object *ClassDB::_instantiate_internal(const StringName &p_class, bool p_require_real_class) { +Object *ClassDB::_instantiate_internal(const StringName &p_class, bool p_require_real_class, bool p_notify_postinitialize) { ClassInfo *ti; { OBJTYPE_RLOCK; ti = classes.getptr(p_class); - if (!ti || ti->disabled || !ti->creation_func || (ti->gdextension && !ti->gdextension->create_instance)) { + if (!_can_instantiate(ti)) { if (compat_classes.has(p_class)) { ti = classes.getptr(compat_classes[p_class]); } @@ -539,36 +555,80 @@ Object *ClassDB::_instantiate_internal(const StringName &p_class, bool p_require ERR_FAIL_COND_V_MSG(ti->disabled, nullptr, "Class '" + String(p_class) + "' is disabled."); ERR_FAIL_NULL_V_MSG(ti->creation_func, nullptr, "Class '" + String(p_class) + "' or its base class cannot be instantiated."); } + #ifdef TOOLS_ENABLED if ((ti->api == API_EDITOR || ti->api == API_EDITOR_EXTENSION) && !Engine::get_singleton()->is_editor_hint()) { ERR_PRINT("Class '" + String(p_class) + "' can only be instantiated by editor."); return nullptr; } #endif - if (ti->gdextension && ti->gdextension->create_instance) { - ObjectGDExtension *extension = ti->gdextension; -#ifdef TOOLS_ENABLED - if (!p_require_real_class && ti->is_runtime && Engine::get_singleton()->is_editor_hint()) { - extension = get_placeholder_extension(ti->name); - } -#endif - return (Object *)extension->create_instance(extension->class_userdata); - } else { + #ifdef TOOLS_ENABLED - if (!p_require_real_class && ti->is_runtime && Engine::get_singleton()->is_editor_hint()) { - if (!ti->inherits_ptr || !ti->inherits_ptr->creation_func) { - ERR_PRINT(vformat("Cannot make a placeholder instance of runtime class %s because its parent cannot be constructed.", ti->name)); - } else { - ObjectGDExtension *extension = get_placeholder_extension(ti->name); - return (Object *)extension->create_instance(extension->class_userdata); + // Try to create placeholder. + if (!p_require_real_class && ti->is_runtime && Engine::get_singleton()->is_editor_hint()) { + bool can_create_placeholder = false; + if (ti->gdextension) { + if (ti->gdextension->create_instance2) { + can_create_placeholder = true; + } +#ifndef DISABLE_DEPRECATED + else if (ti->gdextension->create_instance) { + can_create_placeholder = true; } +#endif // DISABLE_DEPRECATED + } else if (!ti->inherits_ptr || !ti->inherits_ptr->creation_func) { + ERR_PRINT(vformat("Cannot make a placeholder instance of runtime class %s because its parent cannot be constructed.", ti->name)); + } else { + can_create_placeholder = true; } -#endif - return ti->creation_func(); + if (can_create_placeholder) { + ObjectGDExtension *extension = get_placeholder_extension(ti->name); + return (Object *)extension->create_instance2(extension->class_userdata, p_notify_postinitialize); + } + } +#endif // TOOLS_ENABLED + + if (ti->gdextension && ti->gdextension->create_instance2) { + ObjectGDExtension *extension = ti->gdextension; + return (Object *)extension->create_instance2(extension->class_userdata, p_notify_postinitialize); + } +#ifndef DISABLE_DEPRECATED + else if (ti->gdextension && ti->gdextension->create_instance) { + ObjectGDExtension *extension = ti->gdextension; + return (Object *)extension->create_instance(extension->class_userdata); + } +#endif // DISABLE_DEPRECATED + else { + return ti->creation_func(p_notify_postinitialize); } } +bool ClassDB::_can_instantiate(ClassInfo *p_class_info) { + if (!p_class_info) { + return false; + } + + if (p_class_info->disabled || !p_class_info->creation_func) { + return false; + } + + if (!p_class_info->gdextension) { + return true; + } + + if (p_class_info->gdextension->create_instance2) { + return true; + } + +#ifndef DISABLE_DEPRECATED + if (p_class_info->gdextension->create_instance) { + return true; + } +#endif // DISABLE_DEPRECATED + return false; +} + Object *ClassDB::instantiate(const StringName &p_class) { return _instantiate_internal(p_class); } @@ -577,6 +637,10 @@ Object *ClassDB::instantiate_no_placeholders(const StringName &p_class) { return _instantiate_internal(p_class, true); } +Object *ClassDB::instantiate_without_postinitialization(const StringName &p_class) { + return _instantiate_internal(p_class, true, false); +} + #ifdef TOOLS_ENABLED ObjectGDExtension *ClassDB::get_placeholder_extension(const StringName &p_class) { ObjectGDExtension *placeholder_extension = placeholder_extensions.getptr(p_class); @@ -588,7 +652,7 @@ ObjectGDExtension *ClassDB::get_placeholder_extension(const StringName &p_class) { OBJTYPE_RLOCK; ti = classes.getptr(p_class); - if (!ti || ti->disabled || !ti->creation_func || (ti->gdextension && !ti->gdextension->create_instance)) { + if (!_can_instantiate(ti)) { if (compat_classes.has(p_class)) { ti = classes.getptr(compat_classes[p_class]); } @@ -649,7 +713,10 @@ ObjectGDExtension *ClassDB::get_placeholder_extension(const StringName &p_class) placeholder_extension->get_rid = &PlaceholderExtensionInstance::placeholder_instance_get_rid; placeholder_extension->class_userdata = ti; - placeholder_extension->create_instance = &PlaceholderExtensionInstance::placeholder_class_create_instance; +#ifndef DISABLE_DEPRECATED + placeholder_extension->create_instance = nullptr; +#endif // DISABLE_DEPRECATED + placeholder_extension->create_instance2 = &PlaceholderExtensionInstance::placeholder_class_create_instance; placeholder_extension->free_instance = &PlaceholderExtensionInstance::placeholder_class_free_instance; placeholder_extension->get_virtual = &PlaceholderExtensionInstance::placeholder_class_get_virtual; placeholder_extension->get_virtual_call_data = nullptr; @@ -666,7 +733,7 @@ void ClassDB::set_object_extension_instance(Object *p_object, const StringName & { OBJTYPE_RLOCK; ti = classes.getptr(p_class); - if (!ti || ti->disabled || !ti->creation_func || (ti->gdextension && !ti->gdextension->create_instance)) { + if (!_can_instantiate(ti)) { if (compat_classes.has(p_class)) { ti = classes.getptr(compat_classes[p_class]); } @@ -703,7 +770,7 @@ bool ClassDB::can_instantiate(const StringName &p_class) { return false; } #endif - return (!ti->disabled && ti->creation_func != nullptr && !(ti->gdextension && !ti->gdextension->create_instance)); + return _can_instantiate(ti); } bool ClassDB::is_abstract(const StringName &p_class) { @@ -718,7 +785,18 @@ bool ClassDB::is_abstract(const StringName &p_class) { Ref<Script> scr = ResourceLoader::load(path); return scr.is_valid() && scr->is_valid() && scr->is_abstract(); } - return ti->creation_func == nullptr && (!ti->gdextension || ti->gdextension->create_instance == nullptr); + + if (ti->creation_func != nullptr) { + return false; + } + if (!ti->gdextension) { + return true; + } +#ifndef DISABLE_DEPRECATED + return ti->gdextension->create_instance2 == nullptr && ti->gdextension->create_instance == nullptr; +#else + return ti->gdextension->create_instance2 == nullptr; +#endif // DISABLE_DEPRECATED } bool ClassDB::is_virtual(const StringName &p_class) { @@ -738,7 +816,7 @@ bool ClassDB::is_virtual(const StringName &p_class) { return false; } #endif - return (!ti->disabled && ti->creation_func != nullptr && !(ti->gdextension && !ti->gdextension->create_instance) && ti->is_virtual); + return (_can_instantiate(ti) && ti->is_virtual); } void ClassDB::_add_class2(const StringName &p_class, const StringName &p_inherits) { @@ -1570,14 +1648,16 @@ bool ClassDB::get_property(Object *p_object, const StringName &p_property, Varia Variant index = psg->index; const Variant *arg[1] = { &index }; Callable::CallError ce; - r_value = p_object->callp(psg->getter, arg, 1, ce); + const Variant value = p_object->callp(psg->getter, arg, 1, ce); + r_value = (ce.error == Callable::CallError::CALL_OK) ? value : Variant(); } else { Callable::CallError ce; if (psg->_getptr) { r_value = psg->_getptr->call(p_object, nullptr, 0, ce); } else { - r_value = p_object->callp(psg->getter, nullptr, 0, ce); + const Variant value = p_object->callp(psg->getter, nullptr, 0, ce); + r_value = (ce.error == Callable::CallError::CALL_OK) ? value : Variant(); } } return true; diff --git a/core/object/class_db.h b/core/object/class_db.h index d83feafeee..620092a6c4 100644 --- a/core/object/class_db.h +++ b/core/object/class_db.h @@ -134,15 +134,21 @@ public: bool reloadable = false; bool is_virtual = false; bool is_runtime = false; - Object *(*creation_func)() = nullptr; + // The bool argument indicates the need to postinitialize. + Object *(*creation_func)(bool) = nullptr; ClassInfo() {} ~ClassInfo() {} }; template <typename T> - static Object *creator() { - return memnew(T); + static Object *creator(bool p_notify_postinitialize) { + Object *ret = new ("") T; + ret->_initialize(); + if (p_notify_postinitialize) { + ret->_postinitialize(); + } + return ret; } static RWLock lock; @@ -183,7 +189,9 @@ private: static MethodBind *_bind_vararg_method(MethodBind *p_bind, const StringName &p_name, const Vector<Variant> &p_default_args, bool p_compatibility); static void _bind_method_custom(const StringName &p_class, MethodBind *p_method, bool p_compatibility); - static Object *_instantiate_internal(const StringName &p_class, bool p_require_real_class = false); + static Object *_instantiate_internal(const StringName &p_class, bool p_require_real_class = false, bool p_notify_postinitialize = true); + + static bool _can_instantiate(ClassInfo *p_class_info); public: // DO NOT USE THIS!!!!!! NEEDS TO BE PUBLIC BUT DO NOT USE NO MATTER WHAT!!! @@ -256,8 +264,8 @@ public: static void unregister_extension_class(const StringName &p_class, bool p_free_method_binds = true); template <typename T> - static Object *_create_ptr_func() { - return T::create(); + static Object *_create_ptr_func(bool p_notify_postinitialize) { + return T::create(p_notify_postinitialize); } template <typename T> @@ -277,6 +285,7 @@ public: static void get_class_list(List<StringName> *p_classes); #ifdef TOOLS_ENABLED static void get_extensions_class_list(List<StringName> *p_classes); + static void get_extension_class_list(const Ref<GDExtension> &p_extension, List<StringName> *p_classes); static ObjectGDExtension *get_placeholder_extension(const StringName &p_class); #endif static void get_inheriters_from_class(const StringName &p_class, List<StringName> *p_classes); @@ -292,6 +301,7 @@ public: static bool is_virtual(const StringName &p_class); static Object *instantiate(const StringName &p_class); static Object *instantiate_no_placeholders(const StringName &p_class); + static Object *instantiate_without_postinitialization(const StringName &p_class); static void set_object_extension_instance(Object *p_object, const StringName &p_class, GDExtensionClassInstancePtr p_instance); static APIType get_api_type(const StringName &p_class); diff --git a/core/object/object.cpp b/core/object/object.cpp index a2926a478d..8b3ab40271 100644 --- a/core/object/object.cpp +++ b/core/object/object.cpp @@ -207,10 +207,13 @@ void Object::cancel_free() { _predelete_ok = false; } -void Object::_postinitialize() { - _class_name_ptr = _get_class_namev(); // Set the direct pointer, which is much faster to obtain, but can only happen after postinitialize. +void Object::_initialize() { + _class_name_ptr = _get_class_namev(); // Set the direct pointer, which is much faster to obtain, but can only happen after _initialize. _initialize_classv(); _class_name_ptr = nullptr; // May have been called from a constructor. +} + +void Object::_postinitialize() { notification(NOTIFICATION_POSTINITIALIZE); } @@ -743,7 +746,7 @@ Variant Object::callv(const StringName &p_method, const Array &p_args) { } Callable::CallError ce; - Variant ret = callp(p_method, argptrs, p_args.size(), ce); + const Variant ret = callp(p_method, argptrs, p_args.size(), ce); if (ce.error != Callable::CallError::CALL_OK) { ERR_FAIL_V_MSG(Variant(), "Error calling method from 'callv': " + Variant::get_call_error_text(this, p_method, argptrs, p_args.size(), ce) + "."); } @@ -784,7 +787,7 @@ Variant Object::callp(const StringName &p_method, const Variant **p_args, int p_ if (script_instance) { ret = script_instance->callp(p_method, p_args, p_argcount, r_error); - //force jumptable + // Force jump table. switch (r_error.error) { case Callable::CallError::CALL_OK: return ret; @@ -994,7 +997,7 @@ void Object::set_meta(const StringName &p_name, const Variant &p_value) { if (E) { E->value = p_value; } else { - ERR_FAIL_COND_MSG(!p_name.operator String().is_valid_identifier(), "Invalid metadata identifier: '" + p_name + "'."); + ERR_FAIL_COND_MSG(!p_name.operator String().is_valid_ascii_identifier(), "Invalid metadata identifier: '" + p_name + "'."); Variant *V = &metadata.insert(p_name, p_value)->value; const String &sname = p_name; @@ -1020,6 +1023,14 @@ void Object::remove_meta(const StringName &p_name) { set_meta(p_name, Variant()); } +void Object::merge_meta_from(const Object *p_src) { + List<StringName> meta_keys; + p_src->get_meta_list(&meta_keys); + for (const StringName &key : meta_keys) { + set_meta(key, p_src->get_meta(key)); + } +} + TypedArray<Dictionary> Object::_get_property_list_bind() const { List<PropertyInfo> lpi; get_property_list(&lpi); @@ -1901,7 +1912,7 @@ void Object::set_instance_binding(void *p_token, void *p_binding, const GDExtens void *Object::get_instance_binding(void *p_token, const GDExtensionInstanceBindingCallbacks *p_callbacks) { void *binding = nullptr; - _instance_binding_mutex.lock(); + MutexLock instance_binding_lock(_instance_binding_mutex); for (uint32_t i = 0; i < _instance_binding_count; i++) { if (_instance_bindings[i].token == p_token) { binding = _instance_bindings[i].binding; @@ -1932,14 +1943,12 @@ void *Object::get_instance_binding(void *p_token, const GDExtensionInstanceBindi _instance_binding_count++; } - _instance_binding_mutex.unlock(); - return binding; } bool Object::has_instance_binding(void *p_token) { bool found = false; - _instance_binding_mutex.lock(); + MutexLock instance_binding_lock(_instance_binding_mutex); for (uint32_t i = 0; i < _instance_binding_count; i++) { if (_instance_bindings[i].token == p_token) { found = true; @@ -1947,14 +1956,12 @@ bool Object::has_instance_binding(void *p_token) { } } - _instance_binding_mutex.unlock(); - return found; } void Object::free_instance_binding(void *p_token) { bool found = false; - _instance_binding_mutex.lock(); + MutexLock instance_binding_lock(_instance_binding_mutex); for (uint32_t i = 0; i < _instance_binding_count; i++) { if (!found && _instance_bindings[i].token == p_token) { if (_instance_bindings[i].free_callback) { @@ -1973,7 +1980,6 @@ void Object::free_instance_binding(void *p_token) { if (found) { _instance_binding_count--; } - _instance_binding_mutex.unlock(); } #ifdef TOOLS_ENABLED @@ -2129,6 +2135,7 @@ bool predelete_handler(Object *p_object) { } void postinitialize_handler(Object *p_object) { + p_object->_initialize(); p_object->_postinitialize(); } @@ -2290,7 +2297,7 @@ void ObjectDB::cleanup() { // Ensure calling the native classes because if a leaked instance has a script // that overrides any of those methods, it'd not be OK to call them at this point, // now the scripting languages have already been terminated. - MethodBind *node_get_name = ClassDB::get_method("Node", "get_name"); + MethodBind *node_get_path = ClassDB::get_method("Node", "get_path"); MethodBind *resource_get_path = ClassDB::get_method("Resource", "get_path"); Callable::CallError call_error; @@ -2300,7 +2307,7 @@ void ObjectDB::cleanup() { String extra_info; if (obj->is_class("Node")) { - extra_info = " - Node name: " + String(node_get_name->call(obj, nullptr, 0, call_error)); + extra_info = " - Node path: " + String(node_get_path->call(obj, nullptr, 0, call_error)); } if (obj->is_class("Resource")) { extra_info = " - Resource path: " + String(resource_get_path->call(obj, nullptr, 0, call_error)); diff --git a/core/object/object.h b/core/object/object.h index adb50268d2..bc3f663baf 100644 --- a/core/object/object.h +++ b/core/object/object.h @@ -350,7 +350,10 @@ struct ObjectGDExtension { } void *class_userdata = nullptr; +#ifndef DISABLE_DEPRECATED GDExtensionClassCreateInstance create_instance; +#endif // DISABLE_DEPRECATED + GDExtensionClassCreateInstance2 create_instance2; GDExtensionClassFreeInstance free_instance; GDExtensionClassGetVirtual get_virtual; GDExtensionClassGetVirtualCallData get_virtual_call_data; @@ -384,18 +387,6 @@ struct ObjectGDExtension { * much alone defines the object model. */ -#define REVERSE_GET_PROPERTY_LIST \ -public: \ - _FORCE_INLINE_ bool _is_gpl_reversed() const { return true; }; \ - \ -private: - -#define UNREVERSE_GET_PROPERTY_LIST \ -public: \ - _FORCE_INLINE_ bool _is_gpl_reversed() const { return false; }; \ - \ -private: - #define GDCLASS(m_class, m_inherits) \ private: \ void operator=(const m_class &p_rval) {} \ @@ -507,15 +498,10 @@ protected: m_inherits::_get_property_listv(p_list, p_reversed); \ } \ p_list->push_back(PropertyInfo(Variant::NIL, get_class_static(), PROPERTY_HINT_NONE, get_class_static(), PROPERTY_USAGE_CATEGORY)); \ - if (!_is_gpl_reversed()) { \ - ::ClassDB::get_property_list(#m_class, p_list, true, this); \ - } \ + ::ClassDB::get_property_list(#m_class, p_list, true, this); \ if (m_class::_get_get_property_list() != m_inherits::_get_get_property_list()) { \ _get_property_list(p_list); \ } \ - if (_is_gpl_reversed()) { \ - ::ClassDB::get_property_list(#m_class, p_list, true, this); \ - } \ if (p_reversed) { \ m_inherits::_get_property_listv(p_list, p_reversed); \ } \ @@ -632,6 +618,7 @@ private: int _predelete_ok = 0; ObjectID _instance_id; bool _predelete(); + void _initialize(); void _postinitialize(); bool _can_translate = true; bool _emitting = false; @@ -680,7 +667,7 @@ protected: _FORCE_INLINE_ bool _instance_binding_reference(bool p_reference) { bool can_die = true; if (_instance_bindings) { - _instance_binding_mutex.lock(); + MutexLock instance_binding_lock(_instance_binding_mutex); for (uint32_t i = 0; i < _instance_binding_count; i++) { if (_instance_bindings[i].reference_callback) { if (!_instance_bindings[i].reference_callback(_instance_bindings[i].token, _instance_bindings[i].binding, p_reference)) { @@ -688,7 +675,6 @@ protected: } } } - _instance_binding_mutex.unlock(); } return can_die; } @@ -795,8 +781,6 @@ public: return &ptr; } - bool _is_gpl_reversed() const { return false; } - void detach_from_objectdb(); _FORCE_INLINE_ ObjectID get_instance_id() const { return _instance_id; } @@ -883,7 +867,8 @@ public: argptrs[i] = &args[i]; } Callable::CallError cerr; - return callp(p_method, sizeof...(p_args) == 0 ? nullptr : (const Variant **)argptrs, sizeof...(p_args), cerr); + const Variant ret = callp(p_method, sizeof...(p_args) == 0 ? nullptr : (const Variant **)argptrs, sizeof...(p_args), cerr); + return (cerr.error == Callable::CallError::CALL_OK) ? ret : Variant(); } void notification(int p_notification, bool p_reversed = false); @@ -910,6 +895,7 @@ public: MTVIRTUAL void remove_meta(const StringName &p_name); MTVIRTUAL Variant get_meta(const StringName &p_name, const Variant &p_default = Variant()) const; MTVIRTUAL void get_meta_list(List<StringName> *p_list) const; + MTVIRTUAL void merge_meta_from(const Object *p_src); #ifdef TOOLS_ENABLED void set_edited(bool p_edited); diff --git a/core/object/script_language.cpp b/core/object/script_language.cpp index cdc56e5ec5..57e5195137 100644 --- a/core/object/script_language.cpp +++ b/core/object/script_language.cpp @@ -704,6 +704,19 @@ bool PlaceHolderScriptInstance::has_method(const StringName &p_method) const { return false; } +Variant PlaceHolderScriptInstance::callp(const StringName &p_method, const Variant **p_args, int p_argcount, Callable::CallError &r_error) { + r_error.error = Callable::CallError::CALL_ERROR_INVALID_METHOD; +#if TOOLS_ENABLED + if (Engine::get_singleton()->is_editor_hint()) { + return String("Attempt to call a method on a placeholder instance. Check if the script is in tool mode."); + } else { + return String("Attempt to call a method on a placeholder instance. Probably a bug, please report."); + } +#else + return Variant(); +#endif // TOOLS_ENABLED +} + void PlaceHolderScriptInstance::update(const List<PropertyInfo> &p_properties, const HashMap<StringName, Variant> &p_values) { HashSet<StringName> new_values; for (const PropertyInfo &E : p_properties) { diff --git a/core/object/script_language.h b/core/object/script_language.h index 59a43a7b29..e38c344ae5 100644 --- a/core/object/script_language.h +++ b/core/object/script_language.h @@ -454,10 +454,7 @@ public: return 0; } - virtual Variant callp(const StringName &p_method, const Variant **p_args, int p_argcount, Callable::CallError &r_error) override { - r_error.error = Callable::CallError::CALL_ERROR_INVALID_METHOD; - return Variant(); - } + virtual Variant callp(const StringName &p_method, const Variant **p_args, int p_argcount, Callable::CallError &r_error) override; virtual void notification(int p_notification, bool p_reversed = false) override {} virtual Ref<Script> get_script() const override { return script; } diff --git a/core/object/worker_thread_pool.cpp b/core/object/worker_thread_pool.cpp index 56b9fa8475..fe7bbd474c 100644 --- a/core/object/worker_thread_pool.cpp +++ b/core/object/worker_thread_pool.cpp @@ -32,6 +32,7 @@ #include "core/object/script_language.h" #include "core/os/os.h" +#include "core/os/safe_binary_mutex.h" #include "core/os/thread_safe.h" WorkerThreadPool::Task *const WorkerThreadPool::ThreadData::YIELDING = (Task *)1; @@ -46,7 +47,7 @@ void WorkerThreadPool::Task::free_template_userdata() { WorkerThreadPool *WorkerThreadPool::singleton = nullptr; #ifdef THREADS_ENABLED -thread_local uintptr_t WorkerThreadPool::unlockable_mutexes[MAX_UNLOCKABLE_MUTEXES] = {}; +thread_local WorkerThreadPool::UnlockableLocks WorkerThreadPool::unlockable_locks[MAX_UNLOCKABLE_LOCKS]; #endif void WorkerThreadPool::_process_task(Task *p_task) { @@ -126,9 +127,8 @@ void WorkerThreadPool::_process_task(Task *p_task) { if (finished_users == max_users) { // Get rid of the group, because nobody else is using it. - task_mutex.lock(); + MutexLock task_lock(task_mutex); group_allocator.free(p_task->group); - task_mutex.unlock(); } // For groups, tasks get rid of themselves. @@ -348,17 +348,13 @@ WorkerThreadPool::TaskID WorkerThreadPool::add_task(const Callable &p_action, bo } bool WorkerThreadPool::is_task_completed(TaskID p_task_id) const { - task_mutex.lock(); + MutexLock task_lock(task_mutex); const Task *const *taskp = tasks.getptr(p_task_id); if (!taskp) { - task_mutex.unlock(); ERR_FAIL_V_MSG(false, "Invalid Task ID"); // Invalid task } - bool completed = (*taskp)->completed; - task_mutex.unlock(); - - return completed; + return (*taskp)->completed; } Error WorkerThreadPool::wait_for_task_completion(TaskID p_task_id) { @@ -428,13 +424,9 @@ Error WorkerThreadPool::wait_for_task_completion(TaskID p_task_id) { void WorkerThreadPool::_lock_unlockable_mutexes() { #ifdef THREADS_ENABLED - for (uint32_t i = 0; i < MAX_UNLOCKABLE_MUTEXES; i++) { - if (unlockable_mutexes[i]) { - if ((((uintptr_t)unlockable_mutexes[i]) & 1) == 0) { - ((Mutex *)unlockable_mutexes[i])->lock(); - } else { - ((BinaryMutex *)(unlockable_mutexes[i] & ~1))->lock(); - } + for (uint32_t i = 0; i < MAX_UNLOCKABLE_LOCKS; i++) { + if (unlockable_locks[i].ulock) { + unlockable_locks[i].ulock->lock(); } } #endif @@ -442,13 +434,9 @@ void WorkerThreadPool::_lock_unlockable_mutexes() { void WorkerThreadPool::_unlock_unlockable_mutexes() { #ifdef THREADS_ENABLED - for (uint32_t i = 0; i < MAX_UNLOCKABLE_MUTEXES; i++) { - if (unlockable_mutexes[i]) { - if ((((uintptr_t)unlockable_mutexes[i]) & 1) == 0) { - ((Mutex *)unlockable_mutexes[i])->unlock(); - } else { - ((BinaryMutex *)(unlockable_mutexes[i] & ~1))->unlock(); - } + for (uint32_t i = 0; i < MAX_UNLOCKABLE_LOCKS; i++) { + if (unlockable_locks[i].ulock) { + unlockable_locks[i].ulock->unlock(); } } #endif @@ -468,7 +456,10 @@ void WorkerThreadPool::_wait_collaboratively(ThreadData *p_caller_pool_thread, T p_caller_pool_thread->signaled = false; if (IS_WAIT_OVER) { - p_caller_pool_thread->yield_is_over = false; + if (unlikely(p_task == ThreadData::YIELDING)) { + p_caller_pool_thread->yield_is_over = false; + } + if (!exit_threads && was_signaled) { // This thread was awaken for some additional reason, but it's about to exit. // Let's find out what may be pending and forward the requests. @@ -526,10 +517,9 @@ void WorkerThreadPool::yield() { } void WorkerThreadPool::notify_yield_over(TaskID p_task_id) { - task_mutex.lock(); + MutexLock task_lock(task_mutex); Task **taskp = tasks.getptr(p_task_id); if (!taskp) { - task_mutex.unlock(); ERR_FAIL_MSG("Invalid Task ID."); } Task *task = *taskp; @@ -538,7 +528,6 @@ void WorkerThreadPool::notify_yield_over(TaskID p_task_id) { // This avoids a race condition where a task is created and yield-over called before it's processed. task->pending_notify_yield_over = true; } - task_mutex.unlock(); return; } @@ -546,8 +535,6 @@ void WorkerThreadPool::notify_yield_over(TaskID p_task_id) { td.yield_is_over = true; td.signaled = true; td.cond_var.notify_one(); - - task_mutex.unlock(); } WorkerThreadPool::GroupID WorkerThreadPool::_add_group_task(const Callable &p_callable, void (*p_func)(void *, uint32_t), void *p_userdata, BaseTemplateUserdata *p_template_userdata, int p_elements, int p_tasks, bool p_high_priority, const String &p_description) { @@ -605,26 +592,20 @@ WorkerThreadPool::GroupID WorkerThreadPool::add_group_task(const Callable &p_act } uint32_t WorkerThreadPool::get_group_processed_element_count(GroupID p_group) const { - task_mutex.lock(); + MutexLock task_lock(task_mutex); const Group *const *groupp = groups.getptr(p_group); if (!groupp) { - task_mutex.unlock(); ERR_FAIL_V_MSG(0, "Invalid Group ID"); } - uint32_t elements = (*groupp)->completed_index.get(); - task_mutex.unlock(); - return elements; + return (*groupp)->completed_index.get(); } bool WorkerThreadPool::is_group_task_completed(GroupID p_group) const { - task_mutex.lock(); + MutexLock task_lock(task_mutex); const Group *const *groupp = groups.getptr(p_group); if (!groupp) { - task_mutex.unlock(); ERR_FAIL_V_MSG(false, "Invalid Group ID"); } - bool completed = (*groupp)->completed.is_set(); - task_mutex.unlock(); - return completed; + return (*groupp)->completed.is_set(); } void WorkerThreadPool::wait_for_group_task_completion(GroupID p_group) { @@ -648,15 +629,13 @@ void WorkerThreadPool::wait_for_group_task_completion(GroupID p_group) { if (finished_users == max_users) { // All tasks using this group are gone (finished before the group), so clear the group too. - task_mutex.lock(); + MutexLock task_lock(task_mutex); group_allocator.free(group); - task_mutex.unlock(); } } - task_mutex.lock(); // This mutex is needed when Physics 2D and/or 3D is selected to run on a separate thread. + MutexLock task_lock(task_mutex); // This mutex is needed when Physics 2D and/or 3D is selected to run on a separate thread. groups.erase(p_group); - task_mutex.unlock(); #endif } @@ -665,38 +644,38 @@ int WorkerThreadPool::get_thread_index() { return singleton->thread_ids.has(tid) ? singleton->thread_ids[tid] : -1; } -#ifdef THREADS_ENABLED -uint32_t WorkerThreadPool::thread_enter_unlock_allowance_zone(Mutex *p_mutex) { - return _thread_enter_unlock_allowance_zone(p_mutex, false); -} - -uint32_t WorkerThreadPool::thread_enter_unlock_allowance_zone(BinaryMutex *p_mutex) { - return _thread_enter_unlock_allowance_zone(p_mutex, true); +WorkerThreadPool::TaskID WorkerThreadPool::get_caller_task_id() { + int th_index = get_thread_index(); + if (th_index != -1 && singleton->threads[th_index].current_task) { + return singleton->threads[th_index].current_task->self; + } else { + return INVALID_TASK_ID; + } } -uint32_t WorkerThreadPool::_thread_enter_unlock_allowance_zone(void *p_mutex, bool p_is_binary) { - for (uint32_t i = 0; i < MAX_UNLOCKABLE_MUTEXES; i++) { - if (unlikely((unlockable_mutexes[i] & ~1) == (uintptr_t)p_mutex)) { +#ifdef THREADS_ENABLED +uint32_t WorkerThreadPool::_thread_enter_unlock_allowance_zone(THREADING_NAMESPACE::unique_lock<THREADING_NAMESPACE::mutex> &p_ulock) { + for (uint32_t i = 0; i < MAX_UNLOCKABLE_LOCKS; i++) { + DEV_ASSERT((bool)unlockable_locks[i].ulock == (bool)unlockable_locks[i].rc); + if (unlockable_locks[i].ulock == &p_ulock) { // Already registered in the current thread. - return UINT32_MAX; - } - if (!unlockable_mutexes[i]) { - unlockable_mutexes[i] = (uintptr_t)p_mutex; - if (p_is_binary) { - unlockable_mutexes[i] |= 1; - } + unlockable_locks[i].rc++; + return i; + } else if (!unlockable_locks[i].ulock) { + unlockable_locks[i].ulock = &p_ulock; + unlockable_locks[i].rc = 1; return i; } } - ERR_FAIL_V_MSG(UINT32_MAX, "No more unlockable mutex slots available. Engine bug."); + ERR_FAIL_V_MSG(UINT32_MAX, "No more unlockable lock slots available. Engine bug."); } void WorkerThreadPool::thread_exit_unlock_allowance_zone(uint32_t p_zone_id) { - if (p_zone_id == UINT32_MAX) { - return; + DEV_ASSERT(unlockable_locks[p_zone_id].ulock && unlockable_locks[p_zone_id].rc); + unlockable_locks[p_zone_id].rc--; + if (unlockable_locks[p_zone_id].rc == 0) { + unlockable_locks[p_zone_id].ulock = nullptr; } - DEV_ASSERT(unlockable_mutexes[p_zone_id]); - unlockable_mutexes[p_zone_id] = 0; } #endif @@ -708,6 +687,8 @@ void WorkerThreadPool::init(int p_thread_count, float p_low_priority_task_ratio) max_low_priority_threads = CLAMP(p_thread_count * p_low_priority_task_ratio, 1, p_thread_count - 1); + print_verbose(vformat("WorkerThreadPool: %d threads, %d max low-priority.", p_thread_count, max_low_priority_threads)); + threads.resize(p_thread_count); for (uint32_t i = 0; i < threads.size(); i++) { diff --git a/core/object/worker_thread_pool.h b/core/object/worker_thread_pool.h index 8774143abf..5be4f20927 100644 --- a/core/object/worker_thread_pool.h +++ b/core/object/worker_thread_pool.h @@ -162,8 +162,12 @@ private: static WorkerThreadPool *singleton; #ifdef THREADS_ENABLED - static const uint32_t MAX_UNLOCKABLE_MUTEXES = 2; - static thread_local uintptr_t unlockable_mutexes[MAX_UNLOCKABLE_MUTEXES]; + static const uint32_t MAX_UNLOCKABLE_LOCKS = 2; + struct UnlockableLocks { + THREADING_NAMESPACE::unique_lock<THREADING_NAMESPACE::mutex> *ulock = nullptr; + uint32_t rc = 0; + }; + static thread_local UnlockableLocks unlockable_locks[MAX_UNLOCKABLE_LOCKS]; #endif TaskID _add_task(const Callable &p_callable, void (*p_func)(void *), void *p_userdata, BaseTemplateUserdata *p_template_userdata, bool p_high_priority, const String &p_description); @@ -192,7 +196,7 @@ private: void _wait_collaboratively(ThreadData *p_caller_pool_thread, Task *p_task); #ifdef THREADS_ENABLED - static uint32_t _thread_enter_unlock_allowance_zone(void *p_mutex, bool p_is_binary); + static uint32_t _thread_enter_unlock_allowance_zone(THREADING_NAMESPACE::unique_lock<THREADING_NAMESPACE::mutex> &p_ulock); #endif void _lock_unlockable_mutexes(); @@ -239,13 +243,17 @@ public: static WorkerThreadPool *get_singleton() { return singleton; } static int get_thread_index(); + static TaskID get_caller_task_id(); #ifdef THREADS_ENABLED - static uint32_t thread_enter_unlock_allowance_zone(Mutex *p_mutex); - static uint32_t thread_enter_unlock_allowance_zone(BinaryMutex *p_mutex); + _ALWAYS_INLINE_ static uint32_t thread_enter_unlock_allowance_zone(const MutexLock<BinaryMutex> &p_lock) { return _thread_enter_unlock_allowance_zone(p_lock._get_lock()); } + template <int Tag> + _ALWAYS_INLINE_ static uint32_t thread_enter_unlock_allowance_zone(const SafeBinaryMutex<Tag> &p_mutex) { return _thread_enter_unlock_allowance_zone(p_mutex._get_lock()); } static void thread_exit_unlock_allowance_zone(uint32_t p_zone_id); #else - static uint32_t thread_enter_unlock_allowance_zone(void *p_mutex) { return UINT32_MAX; } + static uint32_t thread_enter_unlock_allowance_zone(const MutexLock<BinaryMutex> &p_lock) { return UINT32_MAX; } + template <int Tag> + static uint32_t thread_enter_unlock_allowance_zone(const SafeBinaryMutex<Tag> &p_mutex) { return UINT32_MAX; } static void thread_exit_unlock_allowance_zone(uint32_t p_zone_id) {} #endif diff --git a/core/os/condition_variable.h b/core/os/condition_variable.h index fa1355e98c..c819fa6b40 100644 --- a/core/os/condition_variable.h +++ b/core/os/condition_variable.h @@ -32,6 +32,7 @@ #define CONDITION_VARIABLE_H #include "core/os/mutex.h" +#include "core/os/safe_binary_mutex.h" #ifdef THREADS_ENABLED @@ -56,7 +57,12 @@ class ConditionVariable { public: template <typename BinaryMutexT> _ALWAYS_INLINE_ void wait(const MutexLock<BinaryMutexT> &p_lock) const { - condition.wait(const_cast<THREADING_NAMESPACE::unique_lock<THREADING_NAMESPACE::mutex> &>(p_lock.lock)); + condition.wait(const_cast<THREADING_NAMESPACE::unique_lock<THREADING_NAMESPACE::mutex> &>(p_lock._get_lock())); + } + + template <int Tag> + _ALWAYS_INLINE_ void wait(const MutexLock<SafeBinaryMutex<Tag>> &p_lock) const { + condition.wait(const_cast<THREADING_NAMESPACE::unique_lock<THREADING_NAMESPACE::mutex> &>(p_lock.mutex._get_lock())); } _ALWAYS_INLINE_ void notify_one() const { diff --git a/core/os/mutex.h b/core/os/mutex.h index 3e7aa81bc1..a968fd7029 100644 --- a/core/os/mutex.h +++ b/core/os/mutex.h @@ -72,13 +72,28 @@ public: template <typename MutexT> class MutexLock { - friend class ConditionVariable; - - THREADING_NAMESPACE::unique_lock<typename MutexT::StdMutexType> lock; + mutable THREADING_NAMESPACE::unique_lock<typename MutexT::StdMutexType> lock; public: explicit MutexLock(const MutexT &p_mutex) : lock(p_mutex.mutex) {} + + // Clarification: all the funny syntax is needed so this function exists only for binary mutexes. + template <typename T = MutexT> + _ALWAYS_INLINE_ THREADING_NAMESPACE::unique_lock<THREADING_NAMESPACE::mutex> &_get_lock( + typename std::enable_if<std::is_same<T, THREADING_NAMESPACE::mutex>::value> * = nullptr) const { + return lock; + } + + _ALWAYS_INLINE_ void temp_relock() const { + lock.lock(); + } + + _ALWAYS_INLINE_ void temp_unlock() const { + lock.unlock(); + } + + // TODO: Implement a `try_temp_relock` if needed (will also need a dummy method below). }; using Mutex = MutexImpl<THREADING_NAMESPACE::recursive_mutex>; // Recursive, for general use @@ -104,6 +119,9 @@ template <typename MutexT> class MutexLock { public: MutexLock(const MutexT &p_mutex) {} + + void temp_relock() const {} + void temp_unlock() const {} }; using Mutex = MutexImpl; diff --git a/core/os/os.h b/core/os/os.h index 91e0ce9379..e6ce527720 100644 --- a/core/os/os.h +++ b/core/os/os.h @@ -111,9 +111,6 @@ protected: virtual void initialize() = 0; virtual void initialize_joypads() = 0; - void set_current_rendering_driver_name(const String &p_driver_name) { _current_rendering_driver_name = p_driver_name; } - void set_current_rendering_method(const String &p_name) { _current_rendering_method = p_name; } - void set_display_driver_id(int p_display_driver_id) { _display_driver_id = p_display_driver_id; } virtual void set_main_loop(MainLoop *p_main_loop) = 0; @@ -131,12 +128,16 @@ public: static OS *get_singleton(); + void set_current_rendering_driver_name(const String &p_driver_name) { _current_rendering_driver_name = p_driver_name; } + void set_current_rendering_method(const String &p_name) { _current_rendering_method = p_name; } + String get_current_rendering_driver_name() const { return _current_rendering_driver_name; } String get_current_rendering_method() const { return _current_rendering_method; } int get_display_driver_id() const { return _display_driver_id; } virtual Vector<String> get_video_adapter_driver_info() const = 0; + virtual bool get_user_prefers_integrated_gpu() const { return false; } void print_error(const char *p_function, const char *p_file, int p_line, const char *p_code, const char *p_rationale, bool p_editor_notify = false, Logger::ErrorType p_type = Logger::ERR_ERROR); void print(const char *p_format, ...) _PRINTF_FORMAT_ATTRIBUTE_2_3; diff --git a/core/os/pool_allocator.cpp b/core/os/pool_allocator.cpp deleted file mode 100644 index 9a993cd14f..0000000000 --- a/core/os/pool_allocator.cpp +++ /dev/null @@ -1,588 +0,0 @@ -/**************************************************************************/ -/* pool_allocator.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 "pool_allocator.h" - -#include "core/error/error_macros.h" -#include "core/os/memory.h" -#include "core/os/os.h" -#include "core/string/print_string.h" - -#define COMPACT_CHUNK(m_entry, m_to_pos) \ - if constexpr (true) { \ - void *_dst = &((unsigned char *)pool)[m_to_pos]; \ - void *_src = &((unsigned char *)pool)[(m_entry).pos]; \ - memmove(_dst, _src, aligned((m_entry).len)); \ - (m_entry).pos = m_to_pos; \ - } else \ - ((void)0) - -void PoolAllocator::mt_lock() const { -} - -void PoolAllocator::mt_unlock() const { -} - -bool PoolAllocator::get_free_entry(EntryArrayPos *p_pos) { - if (entry_count == entry_max) { - return false; - } - - for (int i = 0; i < entry_max; i++) { - if (entry_array[i].len == 0) { - *p_pos = i; - return true; - } - } - - ERR_PRINT("Out of memory Chunks!"); - - return false; // -} - -/** - * Find a hole - * @param p_pos The hole is behind the block pointed by this variable upon return. if pos==entry_count, then allocate at end - * @param p_for_size hole size - * @return false if hole found, true if no hole found - */ -bool PoolAllocator::find_hole(EntryArrayPos *p_pos, int p_for_size) { - /* position where previous entry ends. Defaults to zero (begin of pool) */ - - int prev_entry_end_pos = 0; - - for (int i = 0; i < entry_count; i++) { - Entry &entry = entry_array[entry_indices[i]]; - - /* determine hole size to previous entry */ - - int hole_size = entry.pos - prev_entry_end_pos; - - /* determine if what we want fits in that hole */ - if (hole_size >= p_for_size) { - *p_pos = i; - return true; - } - - /* prepare for next one */ - prev_entry_end_pos = entry_end(entry); - } - - /* No holes between entries, check at the end..*/ - - if ((pool_size - prev_entry_end_pos) >= p_for_size) { - *p_pos = entry_count; - return true; - } - - return false; -} - -void PoolAllocator::compact(int p_up_to) { - uint32_t prev_entry_end_pos = 0; - - if (p_up_to < 0) { - p_up_to = entry_count; - } - for (int i = 0; i < p_up_to; i++) { - Entry &entry = entry_array[entry_indices[i]]; - - /* determine hole size to previous entry */ - - int hole_size = entry.pos - prev_entry_end_pos; - - /* if we can compact, do it */ - if (hole_size > 0 && !entry.lock) { - COMPACT_CHUNK(entry, prev_entry_end_pos); - } - - /* prepare for next one */ - prev_entry_end_pos = entry_end(entry); - } -} - -void PoolAllocator::compact_up(int p_from) { - uint32_t next_entry_end_pos = pool_size; // - static_area_size; - - for (int i = entry_count - 1; i >= p_from; i--) { - Entry &entry = entry_array[entry_indices[i]]; - - /* determine hole size for next entry */ - - int hole_size = next_entry_end_pos - (entry.pos + aligned(entry.len)); - - /* if we can compact, do it */ - if (hole_size > 0 && !entry.lock) { - COMPACT_CHUNK(entry, (next_entry_end_pos - aligned(entry.len))); - } - - /* prepare for next one */ - next_entry_end_pos = entry.pos; - } -} - -bool PoolAllocator::find_entry_index(EntryIndicesPos *p_map_pos, const Entry *p_entry) { - EntryArrayPos entry_pos = entry_max; - - for (int i = 0; i < entry_count; i++) { - if (&entry_array[entry_indices[i]] == p_entry) { - entry_pos = i; - break; - } - } - - if (entry_pos == entry_max) { - return false; - } - - *p_map_pos = entry_pos; - return true; -} - -PoolAllocator::ID PoolAllocator::alloc(int p_size) { - ERR_FAIL_COND_V(p_size < 1, POOL_ALLOCATOR_INVALID_ID); - ERR_FAIL_COND_V(p_size > free_mem, POOL_ALLOCATOR_INVALID_ID); - - mt_lock(); - - if (entry_count == entry_max) { - mt_unlock(); - ERR_PRINT("entry_count==entry_max"); - return POOL_ALLOCATOR_INVALID_ID; - } - - int size_to_alloc = aligned(p_size); - - EntryIndicesPos new_entry_indices_pos; - - if (!find_hole(&new_entry_indices_pos, size_to_alloc)) { - /* No hole could be found, try compacting mem */ - compact(); - /* Then search again */ - - if (!find_hole(&new_entry_indices_pos, size_to_alloc)) { - mt_unlock(); - ERR_FAIL_V_MSG(POOL_ALLOCATOR_INVALID_ID, "Memory can't be compacted further."); - } - } - - EntryArrayPos new_entry_array_pos; - - bool found_free_entry = get_free_entry(&new_entry_array_pos); - - if (!found_free_entry) { - mt_unlock(); - ERR_FAIL_V_MSG(POOL_ALLOCATOR_INVALID_ID, "No free entry found in PoolAllocator."); - } - - /* move all entry indices up, make room for this one */ - for (int i = entry_count; i > new_entry_indices_pos; i--) { - entry_indices[i] = entry_indices[i - 1]; - } - - entry_indices[new_entry_indices_pos] = new_entry_array_pos; - - entry_count++; - - Entry &entry = entry_array[entry_indices[new_entry_indices_pos]]; - - entry.len = p_size; - entry.pos = (new_entry_indices_pos == 0) ? 0 : entry_end(entry_array[entry_indices[new_entry_indices_pos - 1]]); //alloc either at beginning or end of previous - entry.lock = 0; - entry.check = (check_count++) & CHECK_MASK; - free_mem -= size_to_alloc; - if (free_mem < free_mem_peak) { - free_mem_peak = free_mem; - } - - ID retval = (entry_indices[new_entry_indices_pos] << CHECK_BITS) | entry.check; - mt_unlock(); - - //ERR_FAIL_COND_V( (uintptr_t)get(retval)%align != 0, retval ); - - return retval; -} - -PoolAllocator::Entry *PoolAllocator::get_entry(ID p_mem) { - unsigned int check = p_mem & CHECK_MASK; - int entry = p_mem >> CHECK_BITS; - ERR_FAIL_INDEX_V(entry, entry_max, nullptr); - ERR_FAIL_COND_V(entry_array[entry].check != check, nullptr); - ERR_FAIL_COND_V(entry_array[entry].len == 0, nullptr); - - return &entry_array[entry]; -} - -const PoolAllocator::Entry *PoolAllocator::get_entry(ID p_mem) const { - unsigned int check = p_mem & CHECK_MASK; - int entry = p_mem >> CHECK_BITS; - ERR_FAIL_INDEX_V(entry, entry_max, nullptr); - ERR_FAIL_COND_V(entry_array[entry].check != check, nullptr); - ERR_FAIL_COND_V(entry_array[entry].len == 0, nullptr); - - return &entry_array[entry]; -} - -void PoolAllocator::free(ID p_mem) { - mt_lock(); - Entry *e = get_entry(p_mem); - if (!e) { - mt_unlock(); - ERR_PRINT("!e"); - return; - } - if (e->lock) { - mt_unlock(); - ERR_PRINT("e->lock"); - return; - } - - EntryIndicesPos entry_indices_pos; - - bool index_found = find_entry_index(&entry_indices_pos, e); - if (!index_found) { - mt_unlock(); - ERR_FAIL_COND(!index_found); - } - - for (int i = entry_indices_pos; i < (entry_count - 1); i++) { - entry_indices[i] = entry_indices[i + 1]; - } - - entry_count--; - free_mem += aligned(e->len); - e->clear(); - mt_unlock(); -} - -int PoolAllocator::get_size(ID p_mem) const { - int size; - mt_lock(); - - const Entry *e = get_entry(p_mem); - if (!e) { - mt_unlock(); - ERR_PRINT("!e"); - return 0; - } - - size = e->len; - - mt_unlock(); - - return size; -} - -Error PoolAllocator::resize(ID p_mem, int p_new_size) { - mt_lock(); - Entry *e = get_entry(p_mem); - - if (!e) { - mt_unlock(); - ERR_FAIL_NULL_V(e, ERR_INVALID_PARAMETER); - } - - if (needs_locking && e->lock) { - mt_unlock(); - ERR_FAIL_COND_V(e->lock, ERR_ALREADY_IN_USE); - } - - uint32_t alloc_size = aligned(p_new_size); - - if ((uint32_t)aligned(e->len) == alloc_size) { - e->len = p_new_size; - mt_unlock(); - return OK; - } else if (e->len > (uint32_t)p_new_size) { - free_mem += aligned(e->len); - free_mem -= alloc_size; - e->len = p_new_size; - mt_unlock(); - return OK; - } - - //p_new_size = align(p_new_size) - int _free = free_mem; // - static_area_size; - - if (uint32_t(_free + aligned(e->len)) < alloc_size) { - mt_unlock(); - ERR_FAIL_V(ERR_OUT_OF_MEMORY); - } - - EntryIndicesPos entry_indices_pos; - - bool index_found = find_entry_index(&entry_indices_pos, e); - - if (!index_found) { - mt_unlock(); - ERR_FAIL_COND_V(!index_found, ERR_BUG); - } - - //no need to move stuff around, it fits before the next block - uint32_t next_pos; - if (entry_indices_pos + 1 == entry_count) { - next_pos = pool_size; // - static_area_size; - } else { - next_pos = entry_array[entry_indices[entry_indices_pos + 1]].pos; - } - - if ((next_pos - e->pos) > alloc_size) { - free_mem += aligned(e->len); - e->len = p_new_size; - free_mem -= alloc_size; - mt_unlock(); - return OK; - } - //it doesn't fit, compact around BEFORE current index (make room behind) - - compact(entry_indices_pos + 1); - - if ((next_pos - e->pos) > alloc_size) { - //now fits! hooray! - free_mem += aligned(e->len); - e->len = p_new_size; - free_mem -= alloc_size; - mt_unlock(); - if (free_mem < free_mem_peak) { - free_mem_peak = free_mem; - } - return OK; - } - - //STILL doesn't fit, compact around AFTER current index (make room after) - - compact_up(entry_indices_pos + 1); - - if ((entry_array[entry_indices[entry_indices_pos + 1]].pos - e->pos) > alloc_size) { - //now fits! hooray! - free_mem += aligned(e->len); - e->len = p_new_size; - free_mem -= alloc_size; - mt_unlock(); - if (free_mem < free_mem_peak) { - free_mem_peak = free_mem; - } - return OK; - } - - mt_unlock(); - ERR_FAIL_V(ERR_OUT_OF_MEMORY); -} - -Error PoolAllocator::lock(ID p_mem) { - if (!needs_locking) { - return OK; - } - mt_lock(); - Entry *e = get_entry(p_mem); - if (!e) { - mt_unlock(); - ERR_PRINT("!e"); - return ERR_INVALID_PARAMETER; - } - e->lock++; - mt_unlock(); - return OK; -} - -bool PoolAllocator::is_locked(ID p_mem) const { - if (!needs_locking) { - return false; - } - - mt_lock(); - const Entry *e = const_cast<PoolAllocator *>(this)->get_entry(p_mem); - if (!e) { - mt_unlock(); - ERR_PRINT("!e"); - return false; - } - bool locked = e->lock; - mt_unlock(); - return locked; -} - -const void *PoolAllocator::get(ID p_mem) const { - if (!needs_locking) { - const Entry *e = get_entry(p_mem); - ERR_FAIL_NULL_V(e, nullptr); - return &pool[e->pos]; - } - - mt_lock(); - const Entry *e = get_entry(p_mem); - - if (!e) { - mt_unlock(); - ERR_FAIL_NULL_V(e, nullptr); - } - if (e->lock == 0) { - mt_unlock(); - ERR_PRINT("e->lock == 0"); - return nullptr; - } - - if ((int)e->pos >= pool_size) { - mt_unlock(); - ERR_PRINT("e->pos<0 || e->pos>=pool_size"); - return nullptr; - } - const void *ptr = &pool[e->pos]; - - mt_unlock(); - - return ptr; -} - -void *PoolAllocator::get(ID p_mem) { - if (!needs_locking) { - Entry *e = get_entry(p_mem); - ERR_FAIL_NULL_V(e, nullptr); - return &pool[e->pos]; - } - - mt_lock(); - Entry *e = get_entry(p_mem); - - if (!e) { - mt_unlock(); - ERR_FAIL_NULL_V(e, nullptr); - } - if (e->lock == 0) { - mt_unlock(); - ERR_PRINT("e->lock == 0"); - return nullptr; - } - - if ((int)e->pos >= pool_size) { - mt_unlock(); - ERR_PRINT("e->pos<0 || e->pos>=pool_size"); - return nullptr; - } - void *ptr = &pool[e->pos]; - - mt_unlock(); - - return ptr; -} - -void PoolAllocator::unlock(ID p_mem) { - if (!needs_locking) { - return; - } - mt_lock(); - Entry *e = get_entry(p_mem); - if (!e) { - mt_unlock(); - ERR_FAIL_NULL(e); - } - if (e->lock == 0) { - mt_unlock(); - ERR_PRINT("e->lock == 0"); - return; - } - e->lock--; - mt_unlock(); -} - -int PoolAllocator::get_used_mem() const { - return pool_size - free_mem; -} - -int PoolAllocator::get_free_peak() { - return free_mem_peak; -} - -int PoolAllocator::get_free_mem() { - return free_mem; -} - -void PoolAllocator::create_pool(void *p_mem, int p_size, int p_max_entries) { - pool = (uint8_t *)p_mem; - pool_size = p_size; - - entry_array = memnew_arr(Entry, p_max_entries); - entry_indices = memnew_arr(int, p_max_entries); - entry_max = p_max_entries; - entry_count = 0; - - free_mem = p_size; - free_mem_peak = p_size; - - check_count = 0; -} - -PoolAllocator::PoolAllocator(int p_size, bool p_needs_locking, int p_max_entries) { - mem_ptr = memalloc(p_size); - ERR_FAIL_NULL(mem_ptr); - align = 1; - create_pool(mem_ptr, p_size, p_max_entries); - needs_locking = p_needs_locking; -} - -PoolAllocator::PoolAllocator(void *p_mem, int p_size, int p_align, bool p_needs_locking, int p_max_entries) { - if (p_align > 1) { - uint8_t *mem8 = (uint8_t *)p_mem; - uint64_t ofs = (uint64_t)mem8; - if (ofs % p_align) { - int dif = p_align - (ofs % p_align); - mem8 += p_align - (ofs % p_align); - p_size -= dif; - p_mem = (void *)mem8; - } - } - - create_pool(p_mem, p_size, p_max_entries); - needs_locking = p_needs_locking; - align = p_align; - mem_ptr = nullptr; -} - -PoolAllocator::PoolAllocator(int p_align, int p_size, bool p_needs_locking, int p_max_entries) { - ERR_FAIL_COND(p_align < 1); - mem_ptr = Memory::alloc_static(p_size + p_align, true); - uint8_t *mem8 = (uint8_t *)mem_ptr; - uint64_t ofs = (uint64_t)mem8; - if (ofs % p_align) { - mem8 += p_align - (ofs % p_align); - } - create_pool(mem8, p_size, p_max_entries); - needs_locking = p_needs_locking; - align = p_align; -} - -PoolAllocator::~PoolAllocator() { - if (mem_ptr) { - memfree(mem_ptr); - } - - memdelete_arr(entry_array); - memdelete_arr(entry_indices); -} diff --git a/core/os/pool_allocator.h b/core/os/pool_allocator.h deleted file mode 100644 index be8b6e061b..0000000000 --- a/core/os/pool_allocator.h +++ /dev/null @@ -1,148 +0,0 @@ -/**************************************************************************/ -/* pool_allocator.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. */ -/**************************************************************************/ - -#ifndef POOL_ALLOCATOR_H -#define POOL_ALLOCATOR_H - -#include "core/typedefs.h" - -/** - * Generic Pool Allocator. - * This is a generic memory pool allocator, with locking, compacting and alignment. (@TODO alignment) - * It used as a standard way to manage allocation in a specific region of memory, such as texture memory, - * audio sample memory, or just any kind of memory overall. - * (@TODO) abstraction should be greater, because in many platforms, you need to manage a nonreachable memory. - */ - -enum { - POOL_ALLOCATOR_INVALID_ID = -1 ///< default invalid value. use INVALID_ID( id ) to test -}; - -class PoolAllocator { -public: - typedef int ID; - -private: - enum { - CHECK_BITS = 8, - CHECK_LEN = (1 << CHECK_BITS), - CHECK_MASK = CHECK_LEN - 1 - - }; - - struct Entry { - unsigned int pos = 0; - unsigned int len = 0; - unsigned int lock = 0; - unsigned int check = 0; - - inline void clear() { - pos = 0; - len = 0; - lock = 0; - check = 0; - } - Entry() {} - }; - - typedef int EntryArrayPos; - typedef int EntryIndicesPos; - - Entry *entry_array = nullptr; - int *entry_indices = nullptr; - int entry_max = 0; - int entry_count = 0; - - uint8_t *pool = nullptr; - void *mem_ptr = nullptr; - int pool_size = 0; - - int free_mem = 0; - int free_mem_peak = 0; - - unsigned int check_count = 0; - int align = 1; - - bool needs_locking = false; - - inline int entry_end(const Entry &p_entry) const { - return p_entry.pos + aligned(p_entry.len); - } - inline int aligned(int p_size) const { - int rem = p_size % align; - if (rem) { - p_size += align - rem; - } - - return p_size; - } - - void compact(int p_up_to = -1); - void compact_up(int p_from = 0); - bool get_free_entry(EntryArrayPos *p_pos); - bool find_hole(EntryArrayPos *p_pos, int p_for_size); - bool find_entry_index(EntryIndicesPos *p_map_pos, const Entry *p_entry); - Entry *get_entry(ID p_mem); - const Entry *get_entry(ID p_mem) const; - - void create_pool(void *p_mem, int p_size, int p_max_entries); - -protected: - virtual void mt_lock() const; ///< Reimplement for custom mt locking - virtual void mt_unlock() const; ///< Reimplement for custom mt locking - -public: - enum { - DEFAULT_MAX_ALLOCS = 4096, - }; - - ID alloc(int p_size); ///< Alloc memory, get an ID on success, POOL_ALOCATOR_INVALID_ID on failure - void free(ID p_mem); ///< Free allocated memory - Error resize(ID p_mem, int p_new_size); ///< resize a memory chunk - int get_size(ID p_mem) const; - - int get_free_mem(); ///< get free memory - int get_used_mem() const; - int get_free_peak(); ///< get free memory - - Error lock(ID p_mem); //@todo move this out - void *get(ID p_mem); - const void *get(ID p_mem) const; - void unlock(ID p_mem); - bool is_locked(ID p_mem) const; - - PoolAllocator(int p_size, bool p_needs_locking = false, int p_max_entries = DEFAULT_MAX_ALLOCS); - PoolAllocator(void *p_mem, int p_size, int p_align = 1, bool p_needs_locking = false, int p_max_entries = DEFAULT_MAX_ALLOCS); - PoolAllocator(int p_align, int p_size, bool p_needs_locking = false, int p_max_entries = DEFAULT_MAX_ALLOCS); - - virtual ~PoolAllocator(); -}; - -#endif // POOL_ALLOCATOR_H diff --git a/core/os/safe_binary_mutex.h b/core/os/safe_binary_mutex.h index 1e98cc074c..74a20043a3 100644 --- a/core/os/safe_binary_mutex.h +++ b/core/os/safe_binary_mutex.h @@ -37,6 +37,11 @@ #ifdef THREADS_ENABLED +#ifdef __clang__ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wundefined-var-template" +#endif + // A very special kind of mutex, used in scenarios where these // requirements hold at the same time: // - Must be used with a condition variable (only binary mutexes are suitable). @@ -47,69 +52,90 @@ // Also, don't forget to declare the thread_local variable on each use. template <int Tag> class SafeBinaryMutex { - friend class MutexLock<SafeBinaryMutex>; + friend class MutexLock<SafeBinaryMutex<Tag>>; using StdMutexType = THREADING_NAMESPACE::mutex; mutable THREADING_NAMESPACE::mutex mutex; - static thread_local uint32_t count; + + struct TLSData { + mutable THREADING_NAMESPACE::unique_lock<THREADING_NAMESPACE::mutex> lock; + uint32_t count = 0; + + TLSData(SafeBinaryMutex<Tag> &p_mutex) : + lock(p_mutex.mutex, THREADING_NAMESPACE::defer_lock) {} + }; + static thread_local TLSData tls_data; public: _ALWAYS_INLINE_ void lock() const { - if (++count == 1) { - mutex.lock(); + if (++tls_data.count == 1) { + tls_data.lock.lock(); } } _ALWAYS_INLINE_ void unlock() const { - DEV_ASSERT(count); - if (--count == 0) { - mutex.unlock(); + DEV_ASSERT(tls_data.count); + if (--tls_data.count == 0) { + tls_data.lock.unlock(); } } - _ALWAYS_INLINE_ bool try_lock() const { - if (count) { - count++; - return true; - } else { - if (mutex.try_lock()) { - count++; - return true; - } else { - return false; - } - } + _ALWAYS_INLINE_ THREADING_NAMESPACE::unique_lock<THREADING_NAMESPACE::mutex> &_get_lock() const { + return const_cast<THREADING_NAMESPACE::unique_lock<THREADING_NAMESPACE::mutex> &>(tls_data.lock); + } + + _ALWAYS_INLINE_ SafeBinaryMutex() { } - ~SafeBinaryMutex() { - DEV_ASSERT(!count); + _ALWAYS_INLINE_ ~SafeBinaryMutex() { + DEV_ASSERT(!tls_data.count); } }; -// This specialization is needed so manual locking and MutexLock can be used -// at the same time on a SafeBinaryMutex. template <int Tag> class MutexLock<SafeBinaryMutex<Tag>> { friend class ConditionVariable; - THREADING_NAMESPACE::unique_lock<THREADING_NAMESPACE::mutex> lock; + const SafeBinaryMutex<Tag> &mutex; public: - _ALWAYS_INLINE_ explicit MutexLock(const SafeBinaryMutex<Tag> &p_mutex) : - lock(p_mutex.mutex) { - SafeBinaryMutex<Tag>::count++; - }; - _ALWAYS_INLINE_ ~MutexLock() { - SafeBinaryMutex<Tag>::count--; - }; + explicit MutexLock(const SafeBinaryMutex<Tag> &p_mutex) : + mutex(p_mutex) { + mutex.lock(); + } + + ~MutexLock() { + mutex.unlock(); + } + + _ALWAYS_INLINE_ void temp_relock() const { + mutex.lock(); + } + + _ALWAYS_INLINE_ void temp_unlock() const { + mutex.unlock(); + } + + // TODO: Implement a `try_temp_relock` if needed (will also need a dummy method below). }; +#ifdef __clang__ +#pragma clang diagnostic pop +#endif + #else // No threads. template <int Tag> -class SafeBinaryMutex : public MutexImpl { - static thread_local uint32_t count; +class SafeBinaryMutex { + struct TLSData { + TLSData(SafeBinaryMutex<Tag> &p_mutex) {} + }; + static thread_local TLSData tls_data; + +public: + void lock() const {} + void unlock() const {} }; template <int Tag> @@ -117,6 +143,9 @@ class MutexLock<SafeBinaryMutex<Tag>> { public: MutexLock(const SafeBinaryMutex<Tag> &p_mutex) {} ~MutexLock() {} + + void temp_relock() const {} + void temp_unlock() const {} }; #endif // THREADS_ENABLED diff --git a/core/string/node_path.cpp b/core/string/node_path.cpp index 8ae2efb787..fdc72bc8dc 100644 --- a/core/string/node_path.cpp +++ b/core/string/node_path.cpp @@ -215,7 +215,10 @@ StringName NodePath::get_concatenated_names() const { String concatenated; const StringName *sn = data->path.ptr(); for (int i = 0; i < pc; i++) { - concatenated += i == 0 ? sn[i].operator String() : "/" + sn[i]; + if (i > 0) { + concatenated += "/"; + } + concatenated += sn[i].operator String(); } data->concatenated_path = concatenated; } @@ -230,7 +233,10 @@ StringName NodePath::get_concatenated_subnames() const { String concatenated; const StringName *ssn = data->subpath.ptr(); for (int i = 0; i < spc; i++) { - concatenated += i == 0 ? ssn[i].operator String() : ":" + ssn[i]; + if (i > 0) { + concatenated += ":"; + } + concatenated += ssn[i].operator String(); } data->concatenated_subpath = concatenated; } diff --git a/core/string/string_name.cpp b/core/string/string_name.cpp index 5d59d65f92..28077fc8c5 100644 --- a/core/string/string_name.cpp +++ b/core/string/string_name.cpp @@ -191,11 +191,10 @@ StringName::StringName(const StringName &p_name) { } void StringName::assign_static_unique_class_name(StringName *ptr, const char *p_name) { - mutex.lock(); + MutexLock lock(mutex); if (*ptr == StringName()) { *ptr = StringName(p_name, true); } - mutex.unlock(); } StringName::StringName(const char *p_name, bool p_static) { diff --git a/core/string/translation_server.cpp b/core/string/translation_server.cpp index 6e784881d0..4ac79ad10a 100644 --- a/core/string/translation_server.cpp +++ b/core/string/translation_server.cpp @@ -284,6 +284,11 @@ String TranslationServer::_standardize_locale(const String &p_locale, bool p_add } int TranslationServer::compare_locales(const String &p_locale_a, const String &p_locale_b) const { + if (p_locale_a == p_locale_b) { + // Exact match. + return 10; + } + String locale_a = _standardize_locale(p_locale_a, true); String locale_b = _standardize_locale(p_locale_b, true); diff --git a/core/string/ustring.cpp b/core/string/ustring.cpp index 2cfc48d395..2683addd4b 100644 --- a/core/string/ustring.cpp +++ b/core/string/ustring.cpp @@ -1694,30 +1694,40 @@ char32_t String::char_lowercase(char32_t p_char) { } String String::to_upper() const { - String upper = *this; + if (is_empty()) { + return *this; + } - for (int i = 0; i < upper.size(); i++) { - const char32_t s = upper[i]; - const char32_t t = _find_upper(s); - if (s != t) { // avoid copy on write - upper[i] = t; - } + String upper; + upper.resize(size()); + const char32_t *old_ptr = ptr(); + char32_t *upper_ptrw = upper.ptrw(); + + while (*old_ptr) { + *upper_ptrw++ = _find_upper(*old_ptr++); } + *upper_ptrw = 0; + return upper; } String String::to_lower() const { - String lower = *this; + if (is_empty()) { + return *this; + } - for (int i = 0; i < lower.size(); i++) { - const char32_t s = lower[i]; - const char32_t t = _find_lower(s); - if (s != t) { // avoid copy on write - lower[i] = t; - } + String lower; + lower.resize(size()); + const char32_t *old_ptr = ptr(); + char32_t *lower_ptrw = lower.ptrw(); + + while (*old_ptr) { + *lower_ptrw++ = _find_lower(*old_ptr++); } + *lower_ptrw = 0; + return lower; } @@ -1955,15 +1965,16 @@ String String::hex_encode_buffer(const uint8_t *p_buffer, int p_len) { static const char hex[16] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; String ret; - char v[2] = { 0, 0 }; + ret.resize(p_len * 2 + 1); + char32_t *ret_ptrw = ret.ptrw(); for (int i = 0; i < p_len; i++) { - v[0] = hex[p_buffer[i] >> 4]; - ret += v; - v[0] = hex[p_buffer[i] & 0xF]; - ret += v; + *ret_ptrw++ = hex[p_buffer[i] >> 4]; + *ret_ptrw++ = hex[p_buffer[i] & 0xF]; } + *ret_ptrw = 0; + return ret; } @@ -1986,11 +1997,12 @@ Vector<uint8_t> String::hex_decode() const { Vector<uint8_t> out; int len = length() / 2; out.resize(len); + uint8_t *out_ptrw = out.ptrw(); for (int i = 0; i < len; i++) { char32_t c; HEX_TO_BYTE(first, i * 2); HEX_TO_BYTE(second, i * 2 + 1); - out.write[i] = first * 16 + second; + out_ptrw[i] = first * 16 + second; } return out; #undef HEX_TO_BYTE @@ -2011,14 +2023,16 @@ CharString String::ascii(bool p_allow_extended) const { CharString cs; cs.resize(size()); + char *cs_ptrw = cs.ptrw(); + const char32_t *this_ptr = ptr(); for (int i = 0; i < size(); i++) { - char32_t c = operator[](i); + char32_t c = this_ptr[i]; if ((c <= 0x7f) || (c <= 0xff && p_allow_extended)) { - cs[i] = c; + cs_ptrw[i] = c; } else { print_unicode_error(vformat("Invalid unicode codepoint (%x), cannot represent as ASCII/Latin-1", (uint32_t)c)); - cs[i] = 0x20; // ascii doesn't have a replacement character like unicode, 0x1a is sometimes used but is kinda arcane + cs_ptrw[i] = 0x20; // ASCII doesn't have a replacement character like unicode, 0x1a is sometimes used but is kinda arcane. } } @@ -3151,8 +3165,9 @@ Vector<uint8_t> String::md5_buffer() const { Vector<uint8_t> ret; ret.resize(16); + uint8_t *ret_ptrw = ret.ptrw(); for (int i = 0; i < 16; i++) { - ret.write[i] = hash[i]; + ret_ptrw[i] = hash[i]; } return ret; } @@ -3164,8 +3179,9 @@ Vector<uint8_t> String::sha1_buffer() const { Vector<uint8_t> ret; ret.resize(20); + uint8_t *ret_ptrw = ret.ptrw(); for (int i = 0; i < 20; i++) { - ret.write[i] = hash[i]; + ret_ptrw[i] = hash[i]; } return ret; @@ -3178,8 +3194,9 @@ Vector<uint8_t> String::sha256_buffer() const { Vector<uint8_t> ret; ret.resize(32); + uint8_t *ret_ptrw = ret.ptrw(); for (int i = 0; i < 32; i++) { - ret.write[i] = hash[i]; + ret_ptrw[i] = hash[i]; } return ret; } @@ -3917,8 +3934,9 @@ Vector<String> String::bigrams() const { return b; } b.resize(n_pairs); + String *b_ptrw = b.ptrw(); for (int i = 0; i < n_pairs; i++) { - b.write[i] = substr(i, 2); + b_ptrw[i] = substr(i, 2); } return b; } @@ -4606,7 +4624,7 @@ bool String::is_absolute_path() const { } } -String String::validate_identifier() const { +String String::validate_ascii_identifier() const { if (is_empty()) { return "_"; // Empty string is not a valid identifier; } @@ -4629,7 +4647,7 @@ String String::validate_identifier() const { return result; } -bool String::is_valid_identifier() const { +bool String::is_valid_ascii_identifier() const { int len = length(); if (len == 0) { @@ -4651,6 +4669,26 @@ bool String::is_valid_identifier() const { return true; } +bool String::is_valid_unicode_identifier() const { + const char32_t *str = ptr(); + int len = length(); + + if (len == 0) { + return false; // Empty string. + } + + if (!is_unicode_identifier_start(str[0])) { + return false; + } + + for (int i = 1; i < len; i++) { + if (!is_unicode_identifier_continue(str[i])) { + return false; + } + } + return true; +} + bool String::is_valid_string() const { int l = length(); const char32_t *src = get_data(); @@ -4897,8 +4935,9 @@ String String::xml_unescape() const { return String(); } str.resize(len + 1); - _xml_unescape(get_data(), l, str.ptrw()); - str[len] = 0; + char32_t *str_ptrw = str.ptrw(); + _xml_unescape(get_data(), l, str_ptrw); + str_ptrw[len] = 0; return str; } diff --git a/core/string/ustring.h b/core/string/ustring.h index 9df2d56e80..11f15031f9 100644 --- a/core/string/ustring.h +++ b/core/string/ustring.h @@ -459,10 +459,11 @@ public: // node functions static String get_invalid_node_name_characters(bool p_allow_internal = false); String validate_node_name() const; - String validate_identifier() const; + String validate_ascii_identifier() const; String validate_filename() const; - bool is_valid_identifier() const; + bool is_valid_ascii_identifier() const; + bool is_valid_unicode_identifier() const; bool is_valid_int() const; bool is_valid_float() const; bool is_valid_hex_number(bool p_with_prefix) const; @@ -470,6 +471,9 @@ public: bool is_valid_ip_address() const; bool is_valid_filename() const; + // Use `is_valid_ascii_identifier()` instead. Kept for compatibility. + bool is_valid_identifier() const { return is_valid_ascii_identifier(); } + /** * The constructors must not depend on other overloads */ diff --git a/core/templates/command_queue_mt.cpp b/core/templates/command_queue_mt.cpp index ef75a70868..5fa767263f 100644 --- a/core/templates/command_queue_mt.cpp +++ b/core/templates/command_queue_mt.cpp @@ -33,14 +33,6 @@ #include "core/config/project_settings.h" #include "core/os/os.h" -void CommandQueueMT::lock() { - mutex.lock(); -} - -void CommandQueueMT::unlock() { - mutex.unlock(); -} - CommandQueueMT::CommandQueueMT() { command_mem.reserve(DEFAULT_COMMAND_MEM_SIZE_KB * 1024); } diff --git a/core/templates/command_queue_mt.h b/core/templates/command_queue_mt.h index 1e6c6e42a9..8ef5dd3064 100644 --- a/core/templates/command_queue_mt.h +++ b/core/templates/command_queue_mt.h @@ -362,23 +362,24 @@ class CommandQueueMT { return; } - lock(); + MutexLock lock(mutex); - uint32_t allowance_id = WorkerThreadPool::thread_enter_unlock_allowance_zone(&mutex); while (flush_read_ptr < command_mem.size()) { uint64_t size = *(uint64_t *)&command_mem[flush_read_ptr]; flush_read_ptr += 8; CommandBase *cmd = reinterpret_cast<CommandBase *>(&command_mem[flush_read_ptr]); + uint32_t allowance_id = WorkerThreadPool::thread_enter_unlock_allowance_zone(lock); cmd->call(); + WorkerThreadPool::thread_exit_unlock_allowance_zone(allowance_id); // Handle potential realloc due to the command and unlock allowance. cmd = reinterpret_cast<CommandBase *>(&command_mem[flush_read_ptr]); if (unlikely(cmd->sync)) { sync_head++; - unlock(); // Give an opportunity to awaiters right away. + lock.~MutexLock(); // Give an opportunity to awaiters right away. sync_cond_var.notify_all(); - lock(); + new (&lock) MutexLock(mutex); // Handle potential realloc happened during unlock. cmd = reinterpret_cast<CommandBase *>(&command_mem[flush_read_ptr]); } @@ -387,14 +388,11 @@ class CommandQueueMT { flush_read_ptr += size; } - WorkerThreadPool::thread_exit_unlock_allowance_zone(allowance_id); command_mem.clear(); flush_read_ptr = 0; _prevent_sync_wraparound(); - - unlock(); } _FORCE_INLINE_ void _wait_for_sync(MutexLock<BinaryMutex> &p_lock) { @@ -410,9 +408,6 @@ class CommandQueueMT { void _no_op() {} public: - void lock(); - void unlock(); - /* NORMAL PUSH COMMANDS */ DECL_PUSH(0) SPACE_SEP_LIST(DECL_PUSH, 15) @@ -446,9 +441,8 @@ public: } void set_pump_task_id(WorkerThreadPool::TaskID p_task_id) { - lock(); + MutexLock lock(mutex); pump_task_id = p_task_id; - unlock(); } CommandQueueMT(); diff --git a/core/templates/paged_allocator.h b/core/templates/paged_allocator.h index 4854e1b866..0b70fa02f3 100644 --- a/core/templates/paged_allocator.h +++ b/core/templates/paged_allocator.h @@ -55,7 +55,7 @@ class PagedAllocator { public: template <typename... Args> T *alloc(Args &&...p_args) { - if (thread_safe) { + if constexpr (thread_safe) { spin_lock.lock(); } if (unlikely(allocs_available == 0)) { @@ -76,7 +76,7 @@ public: allocs_available--; T *alloc = available_pool[allocs_available >> page_shift][allocs_available & page_mask]; - if (thread_safe) { + if constexpr (thread_safe) { spin_lock.unlock(); } memnew_placement(alloc, T(p_args...)); @@ -84,13 +84,13 @@ public: } void free(T *p_mem) { - if (thread_safe) { + if constexpr (thread_safe) { spin_lock.lock(); } p_mem->~T(); available_pool[allocs_available >> page_shift][allocs_available & page_mask] = p_mem; allocs_available++; - if (thread_safe) { + if constexpr (thread_safe) { spin_lock.unlock(); } } @@ -120,28 +120,28 @@ private: public: void reset(bool p_allow_unfreed = false) { - if (thread_safe) { + if constexpr (thread_safe) { spin_lock.lock(); } _reset(p_allow_unfreed); - if (thread_safe) { + if constexpr (thread_safe) { spin_lock.unlock(); } } bool is_configured() const { - if (thread_safe) { + if constexpr (thread_safe) { spin_lock.lock(); } bool result = page_size > 0; - if (thread_safe) { + if constexpr (thread_safe) { spin_lock.unlock(); } return result; } void configure(uint32_t p_page_size) { - if (thread_safe) { + if constexpr (thread_safe) { spin_lock.lock(); } ERR_FAIL_COND(page_pool != nullptr); // Safety check. @@ -149,7 +149,7 @@ public: page_size = nearest_power_of_2_templated(p_page_size); page_mask = page_size - 1; page_shift = get_shift_from_power_of_2(page_size); - if (thread_safe) { + if constexpr (thread_safe) { spin_lock.unlock(); } } @@ -161,7 +161,7 @@ public: } ~PagedAllocator() { - if (thread_safe) { + if constexpr (thread_safe) { spin_lock.lock(); } bool leaked = allocs_available < pages_allocated * page_size; @@ -172,7 +172,7 @@ public: } else { _reset(false); } - if (thread_safe) { + if constexpr (thread_safe) { spin_lock.unlock(); } } diff --git a/core/templates/rid_owner.h b/core/templates/rid_owner.h index 86304d3c73..537413e2ba 100644 --- a/core/templates/rid_owner.h +++ b/core/templates/rid_owner.h @@ -82,7 +82,7 @@ class RID_Alloc : public RID_AllocBase { mutable SpinLock spin_lock; _FORCE_INLINE_ RID _allocate_rid() { - if (THREAD_SAFE) { + if constexpr (THREAD_SAFE) { spin_lock.lock(); } @@ -128,7 +128,7 @@ class RID_Alloc : public RID_AllocBase { alloc_count++; - if (THREAD_SAFE) { + if constexpr (THREAD_SAFE) { spin_lock.unlock(); } @@ -156,14 +156,14 @@ public: if (p_rid == RID()) { return nullptr; } - if (THREAD_SAFE) { + if constexpr (THREAD_SAFE) { spin_lock.lock(); } uint64_t id = p_rid.get_id(); uint32_t idx = uint32_t(id & 0xFFFFFFFF); if (unlikely(idx >= max_alloc)) { - if (THREAD_SAFE) { + if constexpr (THREAD_SAFE) { spin_lock.unlock(); } return nullptr; @@ -176,14 +176,14 @@ public: if (unlikely(p_initialize)) { if (unlikely(!(validator_chunks[idx_chunk][idx_element] & 0x80000000))) { - if (THREAD_SAFE) { + if constexpr (THREAD_SAFE) { spin_lock.unlock(); } ERR_FAIL_V_MSG(nullptr, "Initializing already initialized RID"); } if (unlikely((validator_chunks[idx_chunk][idx_element] & 0x7FFFFFFF) != validator)) { - if (THREAD_SAFE) { + if constexpr (THREAD_SAFE) { spin_lock.unlock(); } ERR_FAIL_V_MSG(nullptr, "Attempting to initialize the wrong RID"); @@ -192,7 +192,7 @@ public: validator_chunks[idx_chunk][idx_element] &= 0x7FFFFFFF; //initialized } else if (unlikely(validator_chunks[idx_chunk][idx_element] != validator)) { - if (THREAD_SAFE) { + if constexpr (THREAD_SAFE) { spin_lock.unlock(); } if ((validator_chunks[idx_chunk][idx_element] & 0x80000000) && validator_chunks[idx_chunk][idx_element] != 0xFFFFFFFF) { @@ -203,7 +203,7 @@ public: T *ptr = &chunks[idx_chunk][idx_element]; - if (THREAD_SAFE) { + if constexpr (THREAD_SAFE) { spin_lock.unlock(); } @@ -221,14 +221,14 @@ public: } _FORCE_INLINE_ bool owns(const RID &p_rid) const { - if (THREAD_SAFE) { + if constexpr (THREAD_SAFE) { spin_lock.lock(); } uint64_t id = p_rid.get_id(); uint32_t idx = uint32_t(id & 0xFFFFFFFF); if (unlikely(idx >= max_alloc)) { - if (THREAD_SAFE) { + if constexpr (THREAD_SAFE) { spin_lock.unlock(); } return false; @@ -241,7 +241,7 @@ public: bool owned = (validator != 0x7FFFFFFF) && (validator_chunks[idx_chunk][idx_element] & 0x7FFFFFFF) == validator; - if (THREAD_SAFE) { + if constexpr (THREAD_SAFE) { spin_lock.unlock(); } @@ -249,14 +249,14 @@ public: } _FORCE_INLINE_ void free(const RID &p_rid) { - if (THREAD_SAFE) { + if constexpr (THREAD_SAFE) { spin_lock.lock(); } uint64_t id = p_rid.get_id(); uint32_t idx = uint32_t(id & 0xFFFFFFFF); if (unlikely(idx >= max_alloc)) { - if (THREAD_SAFE) { + if constexpr (THREAD_SAFE) { spin_lock.unlock(); } ERR_FAIL(); @@ -267,12 +267,12 @@ public: uint32_t validator = uint32_t(id >> 32); if (unlikely(validator_chunks[idx_chunk][idx_element] & 0x80000000)) { - if (THREAD_SAFE) { + if constexpr (THREAD_SAFE) { spin_lock.unlock(); } ERR_FAIL_MSG("Attempted to free an uninitialized or invalid RID."); } else if (unlikely(validator_chunks[idx_chunk][idx_element] != validator)) { - if (THREAD_SAFE) { + if constexpr (THREAD_SAFE) { spin_lock.unlock(); } ERR_FAIL(); @@ -284,7 +284,7 @@ public: alloc_count--; free_list_chunks[alloc_count / elements_in_chunk][alloc_count % elements_in_chunk] = idx; - if (THREAD_SAFE) { + if constexpr (THREAD_SAFE) { spin_lock.unlock(); } } @@ -293,7 +293,7 @@ public: return alloc_count; } void get_owned_list(List<RID> *p_owned) const { - if (THREAD_SAFE) { + if constexpr (THREAD_SAFE) { spin_lock.lock(); } for (size_t i = 0; i < max_alloc; i++) { @@ -302,14 +302,14 @@ public: p_owned->push_back(_make_from_id((validator << 32) | i)); } } - if (THREAD_SAFE) { + if constexpr (THREAD_SAFE) { spin_lock.unlock(); } } //used for fast iteration in the elements or RIDs void fill_owned_buffer(RID *p_rid_buffer) const { - if (THREAD_SAFE) { + if constexpr (THREAD_SAFE) { spin_lock.lock(); } uint32_t idx = 0; @@ -320,7 +320,7 @@ public: idx++; } } - if (THREAD_SAFE) { + if constexpr (THREAD_SAFE) { spin_lock.unlock(); } } diff --git a/core/templates/sort_array.h b/core/templates/sort_array.h index e7eaf8ee81..5bf5b2819d 100644 --- a/core/templates/sort_array.h +++ b/core/templates/sort_array.h @@ -174,14 +174,14 @@ public: while (true) { while (compare(p_array[p_first], p_pivot)) { - if (Validate) { + if constexpr (Validate) { ERR_BAD_COMPARE(p_first == unmodified_last - 1); } p_first++; } p_last--; while (compare(p_pivot, p_array[p_last])) { - if (Validate) { + if constexpr (Validate) { ERR_BAD_COMPARE(p_last == unmodified_first); } p_last--; @@ -251,7 +251,7 @@ public: inline void unguarded_linear_insert(int64_t p_last, T p_value, T *p_array) const { int64_t next = p_last - 1; while (compare(p_value, p_array[next])) { - if (Validate) { + if constexpr (Validate) { ERR_BAD_COMPARE(next == 0); } p_array[p_last] = p_array[next]; diff --git a/core/variant/binder_common.h b/core/variant/binder_common.h index 61b90e2a26..fa49767d46 100644 --- a/core/variant/binder_common.h +++ b/core/variant/binder_common.h @@ -214,11 +214,11 @@ struct VariantCaster<char32_t> { template <> struct PtrToArg<char32_t> { _FORCE_INLINE_ static char32_t convert(const void *p_ptr) { - return char32_t(*reinterpret_cast<const int *>(p_ptr)); + return char32_t(*reinterpret_cast<const int64_t *>(p_ptr)); } typedef int64_t EncodeT; _FORCE_INLINE_ static void encode(char32_t p_val, const void *p_ptr) { - *(int *)p_ptr = p_val; + *(int64_t *)p_ptr = p_val; } }; diff --git a/core/variant/callable.cpp b/core/variant/callable.cpp index 667aae879c..9dff5c1e91 100644 --- a/core/variant/callable.cpp +++ b/core/variant/callable.cpp @@ -112,7 +112,7 @@ Error Callable::rpcp(int p_id, const Variant **p_arguments, int p_argcount, Call argptrs[i + 2] = p_arguments[i]; } - CallError tmp; + CallError tmp; // TODO: Check `tmp`? Error err = (Error)obj->callp(SNAME("rpc_id"), argptrs, argcount, tmp).operator int64_t(); r_call_error.error = Callable::CallError::CALL_OK; diff --git a/core/variant/variant.cpp b/core/variant/variant.cpp index c1ef31c784..186643b024 100644 --- a/core/variant/variant.cpp +++ b/core/variant/variant.cpp @@ -1072,13 +1072,6 @@ bool Variant::is_null() const { } } -bool Variant::initialize_ref(Object *p_object) { - RefCounted *ref_counted = const_cast<RefCounted *>(static_cast<const RefCounted *>(p_object)); - if (!ref_counted->init_ref()) { - return false; - } - return true; -} void Variant::reference(const Variant &p_variant) { switch (type) { case NIL: @@ -2120,7 +2113,7 @@ Variant::operator ::RID() const { } #endif Callable::CallError ce; - Variant ret = _get_obj().obj->callp(CoreStringName(get_rid), nullptr, 0, ce); + const Variant ret = _get_obj().obj->callp(CoreStringName(get_rid), nullptr, 0, ce); if (ce.error == Callable::CallError::CALL_OK && ret.get_type() == Variant::RID) { return ret; } diff --git a/core/variant/variant.h b/core/variant/variant.h index 1cb3580c01..d4e4b330cd 100644 --- a/core/variant/variant.h +++ b/core/variant/variant.h @@ -254,7 +254,6 @@ private: } _data alignas(8); void reference(const Variant &p_variant); - static bool initialize_ref(Object *p_object); void _clear_internal(); diff --git a/core/variant/variant_call.cpp b/core/variant/variant_call.cpp index 695ad23bf5..83f1f981b3 100644 --- a/core/variant/variant_call.cpp +++ b/core/variant/variant_call.cpp @@ -1724,6 +1724,8 @@ static void _register_variant_builtin_methods_string() { bind_string_method(validate_node_name, sarray(), varray()); bind_string_method(validate_filename, sarray(), varray()); + bind_string_method(is_valid_ascii_identifier, sarray(), varray()); + bind_string_method(is_valid_unicode_identifier, sarray(), varray()); bind_string_method(is_valid_identifier, sarray(), varray()); bind_string_method(is_valid_int, sarray(), varray()); bind_string_method(is_valid_float, sarray(), varray()); diff --git a/core/variant/variant_internal.h b/core/variant/variant_internal.h index c52ab6917b..58a45c0a1f 100644 --- a/core/variant/variant_internal.h +++ b/core/variant/variant_internal.h @@ -125,10 +125,6 @@ public: } } - _FORCE_INLINE_ static bool initialize_ref(Object *object) { - return Variant::initialize_ref(object); - } - // Atomic types. _FORCE_INLINE_ static bool *get_bool(Variant *v) { return &v->_data._bool; } _FORCE_INLINE_ static const bool *get_bool(const Variant *v) { return &v->_data._bool; } diff --git a/doc/classes/@GlobalScope.xml b/doc/classes/@GlobalScope.xml index 339bbb71dd..63f5947280 100644 --- a/doc/classes/@GlobalScope.xml +++ b/doc/classes/@GlobalScope.xml @@ -667,12 +667,9 @@ <return type="float" /> <param index="0" name="lin" type="float" /> <description> - Converts from linear energy to decibels (audio). This can be used to implement volume sliders that behave as expected (since volume isn't linear). - [b]Example:[/b] + Converts from linear energy to decibels (audio). Since volume is not normally linear, this can be used to implement volume sliders that behave as expected. + [b]Example:[/b] Change the Master bus's volume through a [Slider] node, which ranges from [code]0.0[/code] to [code]1.0[/code]: [codeblock] - # "Slider" refers to a node that inherits Range such as HSlider or VSlider. - # Its range must be configured to go from 0 to 1. - # Change the bus name if you'd like to change the volume of a specific bus only. AudioServer.set_bus_volume_db(AudioServer.get_bus_index("Master"), linear_to_db($Slider.value)) [/codeblock] </description> @@ -2603,8 +2600,7 @@ </constant> <constant name="OK" value="0" enum="Error"> Methods that return [enum Error] return [constant OK] when no error occurred. - Since [constant OK] has value 0, and all other error constants are positive integers, it can also be used in boolean checks. - [b]Example:[/b] + Since [constant OK] has value [code]0[/code], and all other error constants are positive integers, it can also be used in boolean checks. [codeblock] var error = method_that_returns_error() if error != OK: @@ -2867,7 +2863,7 @@ hintString = $"{Variant.Type.Array:D}:{Variant.Type.Array:D}:{elemType:D}/{elemHint:D}:{elemHintString}"; [/csharp] [/codeblocks] - Examples: + [b]Examples:[/b] [codeblocks] [gdscript] hint_string = "%d:" % [TYPE_INT] # Array of integers. diff --git a/doc/classes/AStar2D.xml b/doc/classes/AStar2D.xml index cfb7d00861..f3a1f6b985 100644 --- a/doc/classes/AStar2D.xml +++ b/doc/classes/AStar2D.xml @@ -22,7 +22,7 @@ <method name="_estimate_cost" qualifiers="virtual const"> <return type="float" /> <param index="0" name="from_id" type="int" /> - <param index="1" name="to_id" type="int" /> + <param index="1" name="end_id" type="int" /> <description> Called when estimating the cost between a point and the path's ending point. Note that this function is hidden in the default [AStar2D] class. diff --git a/doc/classes/AStar3D.xml b/doc/classes/AStar3D.xml index 4448698c32..dad77cc7a8 100644 --- a/doc/classes/AStar3D.xml +++ b/doc/classes/AStar3D.xml @@ -51,7 +51,7 @@ <method name="_estimate_cost" qualifiers="virtual const"> <return type="float" /> <param index="0" name="from_id" type="int" /> - <param index="1" name="to_id" type="int" /> + <param index="1" name="end_id" type="int" /> <description> Called when estimating the cost between a point and the path's ending point. Note that this function is hidden in the default [AStar3D] class. diff --git a/doc/classes/AStarGrid2D.xml b/doc/classes/AStarGrid2D.xml index 5ccc0c8f2a..2ee61bd939 100644 --- a/doc/classes/AStarGrid2D.xml +++ b/doc/classes/AStarGrid2D.xml @@ -41,7 +41,7 @@ <method name="_estimate_cost" qualifiers="virtual const"> <return type="float" /> <param index="0" name="from_id" type="Vector2i" /> - <param index="1" name="to_id" type="Vector2i" /> + <param index="1" name="end_id" type="Vector2i" /> <description> Called when estimating the cost between a point and the path's ending point. Note that this function is hidden in the default [AStarGrid2D] class. @@ -81,6 +81,13 @@ If there is no valid path to the target, and [param allow_partial_path] is [code]true[/code], returns a path to the point closest to the target that can be reached. </description> </method> + <method name="get_point_data_in_region" qualifiers="const"> + <return type="Dictionary[]" /> + <param index="0" name="region" type="Rect2i" /> + <description> + Returns an array of dictionaries with point data ([code]id[/code]: [Vector2i], [code]position[/code]: [Vector2], [code]solid[/code]: [bool], [code]weight_scale[/code]: [float]) within a [param region]. + </description> + </method> <method name="get_point_path"> <return type="PackedVector2Array" /> <param index="0" name="from_id" type="Vector2i" /> diff --git a/doc/classes/AnimatedSprite2D.xml b/doc/classes/AnimatedSprite2D.xml index 012ae4fe29..88e543591a 100644 --- a/doc/classes/AnimatedSprite2D.xml +++ b/doc/classes/AnimatedSprite2D.xml @@ -54,12 +54,10 @@ <param index="0" name="frame" type="int" /> <param index="1" name="progress" type="float" /> <description> - The setter of [member frame] resets the [member frame_progress] to [code]0.0[/code] implicitly, but this method avoids that. - This is useful when you want to carry over the current [member frame_progress] to another [member frame]. - [b]Example:[/b] + Sets [member frame] the [member frame_progress] to the given values. Unlike setting [member frame], this method does not reset the [member frame_progress] to [code]0.0[/code] implicitly. + [b]Example:[/b] Change the animation while keeping the same [member frame] and [member frame_progress]. [codeblocks] [gdscript] - # Change the animation with keeping the frame index and progress. var current_frame = animated_sprite.get_frame() var current_progress = animated_sprite.get_frame_progress() animated_sprite.play("walk_another_skin") diff --git a/doc/classes/AnimatedSprite3D.xml b/doc/classes/AnimatedSprite3D.xml index 5f0a6c3e8d..a466fc32ac 100644 --- a/doc/classes/AnimatedSprite3D.xml +++ b/doc/classes/AnimatedSprite3D.xml @@ -53,12 +53,10 @@ <param index="0" name="frame" type="int" /> <param index="1" name="progress" type="float" /> <description> - The setter of [member frame] resets the [member frame_progress] to [code]0.0[/code] implicitly, but this method avoids that. - This is useful when you want to carry over the current [member frame_progress] to another [member frame]. - [b]Example:[/b] + Sets [member frame] the [member frame_progress] to the given values. Unlike setting [member frame], this method does not reset the [member frame_progress] to [code]0.0[/code] implicitly. + [b]Example:[/b] Change the animation while keeping the same [member frame] and [member frame_progress]. [codeblocks] [gdscript] - # Change the animation with keeping the frame index and progress. var current_frame = animated_sprite.get_frame() var current_progress = animated_sprite.get_frame_progress() animated_sprite.play("walk_another_skin") diff --git a/doc/classes/AnimationMixer.xml b/doc/classes/AnimationMixer.xml index dc1bee4336..d762ffa5a6 100644 --- a/doc/classes/AnimationMixer.xml +++ b/doc/classes/AnimationMixer.xml @@ -376,7 +376,19 @@ </constant> <constant name="ANIMATION_CALLBACK_MODE_DISCRETE_FORCE_CONTINUOUS" value="2" enum="AnimationCallbackModeDiscrete"> Always treat the [constant Animation.UPDATE_DISCRETE] track value as [constant Animation.UPDATE_CONTINUOUS] with [constant Animation.INTERPOLATION_NEAREST]. This is the default behavior for [AnimationTree]. - If a value track has non-numeric type key values, it is internally converted to use [constant ANIMATION_CALLBACK_MODE_DISCRETE_RECESSIVE] with [constant Animation.UPDATE_DISCRETE]. + If a value track has un-interpolatable type key values, it is internally converted to use [constant ANIMATION_CALLBACK_MODE_DISCRETE_RECESSIVE] with [constant Animation.UPDATE_DISCRETE]. + Un-interpolatable type list: + - [constant @GlobalScope.TYPE_NIL] + - [constant @GlobalScope.TYPE_NODE_PATH] + - [constant @GlobalScope.TYPE_RID] + - [constant @GlobalScope.TYPE_OBJECT] + - [constant @GlobalScope.TYPE_CALLABLE] + - [constant @GlobalScope.TYPE_SIGNAL] + - [constant @GlobalScope.TYPE_DICTIONARY] + - [constant @GlobalScope.TYPE_PACKED_BYTE_ARRAY] + [constant @GlobalScope.TYPE_BOOL] and [constant @GlobalScope.TYPE_INT] are treated as [constant @GlobalScope.TYPE_FLOAT] during blending and rounded when the result is retrieved. + It is same for arrays and vectors with them such as [constant @GlobalScope.TYPE_PACKED_INT32_ARRAY] or [constant @GlobalScope.TYPE_VECTOR2I], they are treated as [constant @GlobalScope.TYPE_PACKED_FLOAT32_ARRAY] or [constant @GlobalScope.TYPE_VECTOR2]. Also note that for arrays, the size is also interpolated. + [constant @GlobalScope.TYPE_STRING] and [constant @GlobalScope.TYPE_STRING_NAME] are interpolated between character codes and lengths, but note that there is a difference in algorithm between interpolation between keys and interpolation by blending. </constant> </constants> </class> diff --git a/doc/classes/AnimationNodeStateMachine.xml b/doc/classes/AnimationNodeStateMachine.xml index 86311542ad..e80b1f00b0 100644 --- a/doc/classes/AnimationNodeStateMachine.xml +++ b/doc/classes/AnimationNodeStateMachine.xml @@ -5,7 +5,6 @@ </brief_description> <description> Contains multiple [AnimationRootNode]s representing animation states, connected in a graph. State transitions can be configured to happen automatically or via code, using a shortest-path algorithm. Retrieve the [AnimationNodeStateMachinePlayback] object from the [AnimationTree] node to control it programmatically. - [b]Example:[/b] [codeblocks] [gdscript] var state_machine = $AnimationTree.get("parameters/playback") diff --git a/doc/classes/AnimationNodeStateMachinePlayback.xml b/doc/classes/AnimationNodeStateMachinePlayback.xml index 943e6ed7d9..891dfa9f75 100644 --- a/doc/classes/AnimationNodeStateMachinePlayback.xml +++ b/doc/classes/AnimationNodeStateMachinePlayback.xml @@ -5,7 +5,6 @@ </brief_description> <description> Allows control of [AnimationTree] state machines created with [AnimationNodeStateMachine]. Retrieve with [code]$AnimationTree.get("parameters/playback")[/code]. - [b]Example:[/b] [codeblocks] [gdscript] var state_machine = $AnimationTree.get("parameters/playback") diff --git a/doc/classes/Array.xml b/doc/classes/Array.xml index b60a65e989..f4dcc9bf68 100644 --- a/doc/classes/Array.xml +++ b/doc/classes/Array.xml @@ -4,8 +4,7 @@ A built-in data structure that holds a sequence of elements. </brief_description> <description> - An array data structure that can contain a sequence of elements of any [Variant] type. Elements are accessed by a numerical index starting at 0. Negative indices are used to count from the back (-1 is the last element, -2 is the second to last, etc.). - [b]Example:[/b] + An array data structure that can contain a sequence of elements of any [Variant] type. Elements are accessed by a numerical index starting at [code]0[/code]. Negative indices are used to count from the back ([code]-1[/code] is the last element, [code]-2[/code] is the second to last, etc.). [codeblocks] [gdscript] var array = ["First", 2, 3, "Last"] @@ -728,7 +727,7 @@ print(my_items) # Prints [["Rice", 4], ["Tomato", 5], ["Apple", 9]] # Sort descending, using a lambda function. - my_items.sort_custom(func(a, b): return a[0] > b[0]) + my_items.sort_custom(func(a, b): return a[1] > b[1]) print(my_items) # Prints [["Apple", 9], ["Tomato", 5], ["Rice", 4]] [/codeblock] It may also be necessary to use this method to sort strings by natural order, with [method String.naturalnocasecmp_to], as in the following example: diff --git a/doc/classes/AudioStreamPlayer.xml b/doc/classes/AudioStreamPlayer.xml index eecbb05540..93680de21e 100644 --- a/doc/classes/AudioStreamPlayer.xml +++ b/doc/classes/AudioStreamPlayer.xml @@ -77,7 +77,7 @@ <member name="playback_type" type="int" setter="set_playback_type" getter="get_playback_type" enum="AudioServer.PlaybackType" default="0" experimental=""> The playback type of the stream player. If set other than to the default value, it will force that playback type. </member> - <member name="playing" type="bool" setter="_set_playing" getter="is_playing" default="false"> + <member name="playing" type="bool" setter="set_playing" getter="is_playing" default="false"> If [code]true[/code], this node is playing sounds. Setting this property has the same effect as [method play] and [method stop]. </member> <member name="stream" type="AudioStream" setter="set_stream" getter="get_stream"> diff --git a/doc/classes/AudioStreamPlayer2D.xml b/doc/classes/AudioStreamPlayer2D.xml index a3206ba1d6..71d2e1f0db 100644 --- a/doc/classes/AudioStreamPlayer2D.xml +++ b/doc/classes/AudioStreamPlayer2D.xml @@ -81,7 +81,7 @@ <member name="playback_type" type="int" setter="set_playback_type" getter="get_playback_type" enum="AudioServer.PlaybackType" default="0" experimental=""> The playback type of the stream player. If set other than to the default value, it will force that playback type. </member> - <member name="playing" type="bool" setter="_set_playing" getter="is_playing" default="false"> + <member name="playing" type="bool" setter="set_playing" getter="is_playing" default="false"> If [code]true[/code], audio is playing or is queued to be played (see [method play]). </member> <member name="stream" type="AudioStream" setter="set_stream" getter="get_stream"> diff --git a/doc/classes/AudioStreamPlayer3D.xml b/doc/classes/AudioStreamPlayer3D.xml index bf02caffb4..a14c53bbfb 100644 --- a/doc/classes/AudioStreamPlayer3D.xml +++ b/doc/classes/AudioStreamPlayer3D.xml @@ -102,7 +102,7 @@ <member name="playback_type" type="int" setter="set_playback_type" getter="get_playback_type" enum="AudioServer.PlaybackType" default="0" experimental=""> The playback type of the stream player. If set other than to the default value, it will force that playback type. </member> - <member name="playing" type="bool" setter="_set_playing" getter="is_playing" default="false"> + <member name="playing" type="bool" setter="set_playing" getter="is_playing" default="false"> If [code]true[/code], audio is playing or is queued to be played (see [method play]). </member> <member name="stream" type="AudioStream" setter="set_stream" getter="get_stream"> diff --git a/doc/classes/AudioStreamWAV.xml b/doc/classes/AudioStreamWAV.xml index 8a28514ed6..8d882deaee 100644 --- a/doc/classes/AudioStreamWAV.xml +++ b/doc/classes/AudioStreamWAV.xml @@ -15,7 +15,7 @@ <return type="int" enum="Error" /> <param index="0" name="path" type="String" /> <description> - Saves the AudioStreamWAV as a WAV file to [param path]. Samples with IMA ADPCM or QOA formats can't be saved. + Saves the AudioStreamWAV as a WAV file to [param path]. Samples with IMA ADPCM or Quite OK Audio formats can't be saved. [b]Note:[/b] A [code].wav[/code] extension is automatically appended to [param path] if it is missing. </description> </method> @@ -23,19 +23,20 @@ <members> <member name="data" type="PackedByteArray" setter="set_data" getter="get_data" default="PackedByteArray()"> Contains the audio data in bytes. - [b]Note:[/b] This property expects signed PCM8 data. To convert unsigned PCM8 to signed PCM8, subtract 128 from each byte. + [b]Note:[/b] If [member format] is set to [constant FORMAT_8_BITS], this property expects signed 8-bit PCM data. To convert from unsigned 8-bit PCM, subtract 128 from each byte. + [b]Note:[/b] If [member format] is set to [constant FORMAT_QOA], this property expects data from a full QOA file. </member> <member name="format" type="int" setter="set_format" getter="get_format" enum="AudioStreamWAV.Format" default="0"> Audio format. See [enum Format] constants for values. </member> <member name="loop_begin" type="int" setter="set_loop_begin" getter="get_loop_begin" default="0"> - The loop start point (in number of samples, relative to the beginning of the stream). This information will be imported automatically from the WAV file if present. + The loop start point (in number of samples, relative to the beginning of the stream). </member> <member name="loop_end" type="int" setter="set_loop_end" getter="get_loop_end" default="0"> - The loop end point (in number of samples, relative to the beginning of the stream). This information will be imported automatically from the WAV file if present. + The loop end point (in number of samples, relative to the beginning of the stream). </member> <member name="loop_mode" type="int" setter="set_loop_mode" getter="get_loop_mode" enum="AudioStreamWAV.LoopMode" default="0"> - The loop mode. This information will be imported automatically from the WAV file if present. See [enum LoopMode] constants for values. + The loop mode. See [enum LoopMode] constants for values. </member> <member name="mix_rate" type="int" setter="set_mix_rate" getter="get_mix_rate" default="44100"> The sample rate for mixing this audio. Higher values require more storage space, but result in better quality. @@ -48,16 +49,16 @@ </members> <constants> <constant name="FORMAT_8_BITS" value="0" enum="Format"> - 8-bit audio codec. + 8-bit PCM audio codec. </constant> <constant name="FORMAT_16_BITS" value="1" enum="Format"> - 16-bit audio codec. + 16-bit PCM audio codec. </constant> <constant name="FORMAT_IMA_ADPCM" value="2" enum="Format"> - Audio is compressed using IMA ADPCM. + Audio is lossily compressed as IMA ADPCM. </constant> <constant name="FORMAT_QOA" value="3" enum="Format"> - Audio is compressed as QOA ([url=https://qoaformat.org/]Quite OK Audio[/url]). + Audio is lossily compressed as [url=https://qoaformat.org/]Quite OK Audio[/url]. </constant> <constant name="LOOP_DISABLED" value="0" enum="LoopMode"> Audio does not loop. diff --git a/doc/classes/CPUParticles3D.xml b/doc/classes/CPUParticles3D.xml index 27726ff8a2..d7770f2cd5 100644 --- a/doc/classes/CPUParticles3D.xml +++ b/doc/classes/CPUParticles3D.xml @@ -168,6 +168,10 @@ <member name="emission_ring_axis" type="Vector3" setter="set_emission_ring_axis" getter="get_emission_ring_axis"> The axis of the ring when using the emitter [constant EMISSION_SHAPE_RING]. </member> + <member name="emission_ring_cone_angle" type="float" setter="set_emission_ring_cone_angle" getter="get_emission_ring_cone_angle"> + The angle of the cone when using the emitter [constant EMISSION_SHAPE_RING]. The default angle of 90 degrees results in a ring, while an angle of 0 degrees results in a cone. Intermediate values will result in a ring where one end is larger than the other. + [b]Note:[/b] Depending on [member emission_ring_height], the angle may be clamped if the ring's end is reached to form a perfect cone. + </member> <member name="emission_ring_height" type="float" setter="set_emission_ring_height" getter="get_emission_ring_height"> The height of the ring when using the emitter [constant EMISSION_SHAPE_RING]. </member> diff --git a/doc/classes/Callable.xml b/doc/classes/Callable.xml index 05174abb07..0c8f3c66f5 100644 --- a/doc/classes/Callable.xml +++ b/doc/classes/Callable.xml @@ -5,7 +5,6 @@ </brief_description> <description> [Callable] is a built-in [Variant] type that represents a function. It can either be a method within an [Object] instance, or a custom callable used for different purposes (see [method is_custom]). Like all [Variant] types, it can be stored in variables and passed to other functions. It is most commonly used for signal callbacks. - [b]Example:[/b] [codeblocks] [gdscript] func print_args(arg1, arg2, arg3 = ""): @@ -203,7 +202,8 @@ <method name="is_null" qualifiers="const"> <return type="bool" /> <description> - Returns [code]true[/code] if this [Callable] has no target to call the method on. + Returns [code]true[/code] if this [Callable] has no target to call the method on. Equivalent to [code]callable == Callable()[/code]. + [b]Note:[/b] This is [i]not[/i] the same as [code]not is_valid()[/code] and using [code]not is_null()[/code] will [i]not[/i] guarantee that this callable can be called. Use [method is_valid] instead. </description> </method> <method name="is_standard" qualifiers="const"> diff --git a/doc/classes/CallbackTweener.xml b/doc/classes/CallbackTweener.xml index e6a37a20e1..afb9e70601 100644 --- a/doc/classes/CallbackTweener.xml +++ b/doc/classes/CallbackTweener.xml @@ -16,10 +16,10 @@ <param index="0" name="delay" type="float" /> <description> Makes the callback call delayed by given time in seconds. - [b]Example:[/b] + [b]Example:[/b] Call [method Node.queue_free] after 2 seconds. [codeblock] var tween = get_tree().create_tween() - tween.tween_callback(queue_free).set_delay(2) #this will call queue_free() after 2 seconds + tween.tween_callback(queue_free).set_delay(2) [/codeblock] </description> </method> diff --git a/doc/classes/ClassDB.xml b/doc/classes/ClassDB.xml index 66b67d1a59..99d0c9be84 100644 --- a/doc/classes/ClassDB.xml +++ b/doc/classes/ClassDB.xml @@ -16,6 +16,14 @@ Returns [code]true[/code] if objects can be instantiated from the specified [param class], otherwise returns [code]false[/code]. </description> </method> + <method name="class_call_static_method" qualifiers="vararg"> + <return type="Variant" /> + <param index="0" name="class" type="StringName" /> + <param index="1" name="method" type="StringName" /> + <description> + Calls a static method on a class. + </description> + </method> <method name="class_exists" qualifiers="const"> <return type="bool" /> <param index="0" name="class" type="StringName" /> diff --git a/doc/classes/CodeEdit.xml b/doc/classes/CodeEdit.xml index d455799c29..136b8e5fc5 100644 --- a/doc/classes/CodeEdit.xml +++ b/doc/classes/CodeEdit.xml @@ -721,6 +721,9 @@ <theme_item name="can_fold_code_region" data_type="icon" type="Texture2D"> Sets a custom [Texture2D] to draw in the line folding gutter when a code region can be folded. </theme_item> + <theme_item name="completion_color_bg" data_type="icon" type="Texture2D"> + Background panel for the color preview box in autocompletion (visible when the color is translucent). + </theme_item> <theme_item name="executing_line" data_type="icon" type="Texture2D"> Icon to draw in the executing gutter for executing lines. </theme_item> diff --git a/doc/classes/Control.xml b/doc/classes/Control.xml index a0c76a3ad6..927ab9ae0e 100644 --- a/doc/classes/Control.xml +++ b/doc/classes/Control.xml @@ -1333,13 +1333,14 @@ Tells the parent [Container] to align the node with its end, either the bottom or the right edge. It is mutually exclusive with [constant SIZE_FILL] and other shrink size flags, but can be used with [constant SIZE_EXPAND] in some containers. Use with [member size_flags_horizontal] and [member size_flags_vertical]. </constant> <constant name="MOUSE_FILTER_STOP" value="0" enum="MouseFilter"> - The control will receive mouse movement input events and mouse button input events if clicked on through [method _gui_input]. And the control will receive the [signal mouse_entered] and [signal mouse_exited] signals. These events are automatically marked as handled, and they will not propagate further to other controls. This also results in blocking signals in other controls. + The control will receive mouse movement input events and mouse button input events if clicked on through [method _gui_input]. The control will also receive the [signal mouse_entered] and [signal mouse_exited] signals. These events are automatically marked as handled, and they will not propagate further to other controls. This also results in blocking signals in other controls. </constant> <constant name="MOUSE_FILTER_PASS" value="1" enum="MouseFilter"> - The control will receive mouse movement input events and mouse button input events if clicked on through [method _gui_input]. And the control will receive the [signal mouse_entered] and [signal mouse_exited] signals. If this control does not handle the event, the parent control (if any) will be considered, and so on until there is no more parent control to potentially handle it. This also allows signals to fire in other controls. If no control handled it, the event will be passed to [method Node._shortcut_input] for further processing. + The control will receive mouse movement input events and mouse button input events if clicked on through [method _gui_input]. The control will also receive the [signal mouse_entered] and [signal mouse_exited] signals. + If this control does not handle the event, the event will propagate up to its parent control if it has one. The event is bubbled up the node hierarchy until it reaches a non-[CanvasItem], a control with [constant MOUSE_FILTER_STOP], or a [CanvasItem] with [member CanvasItem.top_level] enabled. This will allow signals to fire in all controls it reaches. If no control handled it, the event will be passed to [method Node._shortcut_input] for further processing. </constant> <constant name="MOUSE_FILTER_IGNORE" value="2" enum="MouseFilter"> - The control will not receive mouse movement input events and mouse button input events if clicked on through [method _gui_input]. The control will also not receive the [signal mouse_entered] nor [signal mouse_exited] signals. This will not block other controls from receiving these events or firing the signals. Ignored events will not be handled automatically. + The control will not receive any mouse movement input events nor mouse button input events through [method _gui_input]. The control will also not receive the [signal mouse_entered] nor [signal mouse_exited] signals. This will not block other controls from receiving these events or firing the signals. Ignored events will not be handled automatically. If a child has [constant MOUSE_FILTER_PASS] and an event was passed to this control, the event will further propagate up to the control's parent. [b]Note:[/b] If the control has received [signal mouse_entered] but not [signal mouse_exited], changing the [member mouse_filter] to [constant MOUSE_FILTER_IGNORE] will cause [signal mouse_exited] to be emitted. </constant> <constant name="GROW_DIRECTION_BEGIN" value="0" enum="GrowDirection"> diff --git a/doc/classes/DisplayServer.xml b/doc/classes/DisplayServer.xml index 0bed5288bd..79064a88ba 100644 --- a/doc/classes/DisplayServer.xml +++ b/doc/classes/DisplayServer.xml @@ -1772,7 +1772,7 @@ <param index="0" name="window_id" type="int" /> <param index="1" name="parent_window_id" type="int" /> <description> - Sets window transient parent. Transient window is will be destroyed with its transient parent and will return focus to their parent when closed. The transient window is displayed on top of a non-exclusive full-screen parent window. Transient windows can't enter full-screen mode. + Sets window transient parent. Transient window will be destroyed with its transient parent and will return focus to their parent when closed. The transient window is displayed on top of a non-exclusive full-screen parent window. Transient windows can't enter full-screen mode. [b]Note:[/b] It's recommended to change this value using [member Window.transient] instead. [b]Note:[/b] The behavior might be different depending on the platform. </description> diff --git a/doc/classes/EditorExportPlatform.xml b/doc/classes/EditorExportPlatform.xml index 0e5de79b25..d4084e84a0 100644 --- a/doc/classes/EditorExportPlatform.xml +++ b/doc/classes/EditorExportPlatform.xml @@ -11,11 +11,217 @@ <link title="Console support in Godot">$DOCS_URL/tutorials/platform/consoles.html</link> </tutorials> <methods> + <method name="add_message"> + <return type="void" /> + <param index="0" name="type" type="int" enum="EditorExportPlatform.ExportMessageType" /> + <param index="1" name="category" type="String" /> + <param index="2" name="message" type="String" /> + <description> + Adds a message to the export log that will be displayed when exporting ends. + </description> + </method> + <method name="clear_messages"> + <return type="void" /> + <description> + Clears the export log. + </description> + </method> + <method name="create_preset"> + <return type="EditorExportPreset" /> + <description> + Create a new preset for this platform. + </description> + </method> + <method name="export_pack"> + <return type="int" enum="Error" /> + <param index="0" name="preset" type="EditorExportPreset" /> + <param index="1" name="debug" type="bool" /> + <param index="2" name="path" type="String" /> + <param index="3" name="flags" type="int" enum="EditorExportPlatform.DebugFlags" is_bitfield="true" default="0" /> + <description> + Creates a PCK archive at [param path] for the specified [param preset]. + </description> + </method> + <method name="export_project"> + <return type="int" enum="Error" /> + <param index="0" name="preset" type="EditorExportPreset" /> + <param index="1" name="debug" type="bool" /> + <param index="2" name="path" type="String" /> + <param index="3" name="flags" type="int" enum="EditorExportPlatform.DebugFlags" is_bitfield="true" default="0" /> + <description> + Creates a full project at [param path] for the specified [param preset]. + </description> + </method> + <method name="export_project_files"> + <return type="int" enum="Error" /> + <param index="0" name="preset" type="EditorExportPreset" /> + <param index="1" name="debug" type="bool" /> + <param index="2" name="save_cb" type="Callable" /> + <param index="3" name="shared_cb" type="Callable" default="Callable()" /> + <description> + Exports project files for the specified preset. This method can be used to implement custom export format, other than PCK and ZIP. One of the callbacks is called for each exported file. + [param save_cb] is called for all exported files and have the following arguments: [code]file_path: String[/code], [code]file_data: PackedByteArray[/code], [code]file_index: int[/code], [code]file_count: int[/code], [code]encryption_include_filters: PackedStringArray[/code], [code]encryption_exclude_filters: PackedStringArray[/code], [code]encryption_key: PackedByteArray[/code]. + [param shared_cb] is called for exported native shared/static libraries and have the following arguments: [code]file_path: String[/code], [code]tags: PackedStringArray[/code], [code]target_folder: String[/code]. + [b]Note:[/b] [code]file_index[/code] and [code]file_count[/code] are intended for progress tracking only and aren't necesserely unique and precise. + </description> + </method> + <method name="export_zip"> + <return type="int" enum="Error" /> + <param index="0" name="preset" type="EditorExportPreset" /> + <param index="1" name="debug" type="bool" /> + <param index="2" name="path" type="String" /> + <param index="3" name="flags" type="int" enum="EditorExportPlatform.DebugFlags" is_bitfield="true" default="0" /> + <description> + Create a ZIP archive at [param path] for the specified [param preset]. + </description> + </method> + <method name="find_export_template" qualifiers="const"> + <return type="Dictionary" /> + <param index="0" name="template_file_name" type="String" /> + <description> + Locates export template for the platform, and returns [Dictionary] with the following keys: [code]path: String[/code] and [code]error: String[/code]. This method is provided for convenience and custom export platforms aren't required to use it or keep export templates stored in the same way official templates are. + </description> + </method> + <method name="gen_export_flags"> + <return type="PackedStringArray" /> + <param index="0" name="flags" type="int" enum="EditorExportPlatform.DebugFlags" is_bitfield="true" /> + <description> + Generates array of command line arguments for the default export templates for the debug flags and editor settings. + </description> + </method> + <method name="get_current_presets" qualifiers="const"> + <return type="Array" /> + <description> + Returns array of [EditorExportPreset]s for this platform. + </description> + </method> + <method name="get_forced_export_files" qualifiers="static"> + <return type="PackedStringArray" /> + <description> + Returns array of core file names that always should be exported regardless of preset config. + </description> + </method> + <method name="get_message_category" qualifiers="const"> + <return type="String" /> + <param index="0" name="index" type="int" /> + <description> + Returns message category, for the message with [param index]. + </description> + </method> + <method name="get_message_count" qualifiers="const"> + <return type="int" /> + <description> + Returns number of messages in the export log. + </description> + </method> + <method name="get_message_text" qualifiers="const"> + <return type="String" /> + <param index="0" name="index" type="int" /> + <description> + Returns message text, for the message with [param index]. + </description> + </method> + <method name="get_message_type" qualifiers="const"> + <return type="int" enum="EditorExportPlatform.ExportMessageType" /> + <param index="0" name="index" type="int" /> + <description> + Returns message type, for the message with [param index]. + </description> + </method> <method name="get_os_name" qualifiers="const"> <return type="String" /> <description> Returns the name of the export operating system handled by this [EditorExportPlatform] class, as a friendly string. Possible return values are [code]Windows[/code], [code]Linux[/code], [code]macOS[/code], [code]Android[/code], [code]iOS[/code], and [code]Web[/code]. </description> </method> + <method name="get_worst_message_type" qualifiers="const"> + <return type="int" enum="EditorExportPlatform.ExportMessageType" /> + <description> + Returns most severe message type currently present in the export log. + </description> + </method> + <method name="save_pack"> + <return type="Dictionary" /> + <param index="0" name="preset" type="EditorExportPreset" /> + <param index="1" name="debug" type="bool" /> + <param index="2" name="path" type="String" /> + <param index="3" name="embed" type="bool" default="false" /> + <description> + Saves PCK archive and returns [Dictionary] with the following keys: [code]result: Error[/code], [code]so_files: Array[/code] (array of the shared/static objects which contains dictionaries with the following keys: [code]path: String[/code], [code]tags: PackedStringArray[/code], and [code]target_folder: String[/code]). + If [param embed] is [code]true[/code], PCK content is appended to the end of [param path] file and return [Dictionary] additionally include following keys: [code]embedded_start: int[/code] (embedded PCK offset) and [code]embedded_size: int[/code] (embedded PCK size). + </description> + </method> + <method name="save_zip"> + <return type="Dictionary" /> + <param index="0" name="preset" type="EditorExportPreset" /> + <param index="1" name="debug" type="bool" /> + <param index="2" name="path" type="String" /> + <description> + Saves ZIP archive and returns [Dictionary] with the following keys: [code]result: Error[/code], [code]so_files: Array[/code] (array of the shared/static objects which contains dictionaries with the following keys: [code]path: String[/code], [code]tags: PackedStringArray[/code], and [code]target_folder: String[/code]). + </description> + </method> + <method name="ssh_push_to_remote" qualifiers="const"> + <return type="int" enum="Error" /> + <param index="0" name="host" type="String" /> + <param index="1" name="port" type="String" /> + <param index="2" name="scp_args" type="PackedStringArray" /> + <param index="3" name="src_file" type="String" /> + <param index="4" name="dst_file" type="String" /> + <description> + Uploads specified file over SCP protocol to the remote host. + </description> + </method> + <method name="ssh_run_on_remote" qualifiers="const"> + <return type="int" enum="Error" /> + <param index="0" name="host" type="String" /> + <param index="1" name="port" type="String" /> + <param index="2" name="ssh_arg" type="PackedStringArray" /> + <param index="3" name="cmd_args" type="String" /> + <param index="4" name="output" type="Array" default="[]" /> + <param index="5" name="port_fwd" type="int" default="-1" /> + <description> + Executes specified command on the remote host via SSH protocol and returns command output in the [param output]. + </description> + </method> + <method name="ssh_run_on_remote_no_wait" qualifiers="const"> + <return type="int" /> + <param index="0" name="host" type="String" /> + <param index="1" name="port" type="String" /> + <param index="2" name="ssh_args" type="PackedStringArray" /> + <param index="3" name="cmd_args" type="String" /> + <param index="4" name="port_fwd" type="int" default="-1" /> + <description> + Executes specified command on the remote host via SSH protocol and returns process ID (on the remote host) without waiting for command to finish. + </description> + </method> </methods> + <constants> + <constant name="EXPORT_MESSAGE_NONE" value="0" enum="ExportMessageType"> + Invalid message type used as the default value when no type is specified. + </constant> + <constant name="EXPORT_MESSAGE_INFO" value="1" enum="ExportMessageType"> + Message type for informational messages that have no effect on the export. + </constant> + <constant name="EXPORT_MESSAGE_WARNING" value="2" enum="ExportMessageType"> + Message type for warning messages that should be addressed but still allow to complete the export. + </constant> + <constant name="EXPORT_MESSAGE_ERROR" value="3" enum="ExportMessageType"> + Message type for error messages that must be addressed and fail the export. + </constant> + <constant name="DEBUG_FLAG_DUMB_CLIENT" value="1" enum="DebugFlags" is_bitfield="true"> + Flag is set if remotely debugged project is expected to use remote file system. If set, [method gen_export_flags] will add [code]--remove-fs[/code] and [code]--remote-fs-password[/code] (if password is set in the editor settings) command line arguments to the list. + </constant> + <constant name="DEBUG_FLAG_REMOTE_DEBUG" value="2" enum="DebugFlags" is_bitfield="true"> + Flag is set if remote debug is enabled. If set, [method gen_export_flags] will add [code]--remote-debug[/code] and [code]--breakpoints[/code] (if breakpoints are selected in the script editor or added by the plugin) command line arguments to the list. + </constant> + <constant name="DEBUG_FLAG_REMOTE_DEBUG_LOCALHOST" value="4" enum="DebugFlags" is_bitfield="true"> + Flag is set if remotely debugged project is running on the localhost. If set, [method gen_export_flags] will use [code]localhost[/code] instead of [member EditorSettings.network/debug/remote_host] as remote debugger host. + </constant> + <constant name="DEBUG_FLAG_VIEW_COLLISIONS" value="8" enum="DebugFlags" is_bitfield="true"> + Flag is set if "Visible Collision Shapes" remote debug option is enabled. If set, [method gen_export_flags] will add [code]--debug-collisions[/code] command line arguments to the list. + </constant> + <constant name="DEBUG_FLAG_VIEW_NAVIGATION" value="16" enum="DebugFlags" is_bitfield="true"> + Flag is set if Visible Navigation" remote debug option is enabled. If set, [method gen_export_flags] will add [code]--debug-navigation[/code] command line arguments to the list. + </constant> + </constants> </class> diff --git a/doc/classes/EditorExportPlatformExtension.xml b/doc/classes/EditorExportPlatformExtension.xml new file mode 100644 index 0000000000..ef589e2f58 --- /dev/null +++ b/doc/classes/EditorExportPlatformExtension.xml @@ -0,0 +1,282 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<class name="EditorExportPlatformExtension" inherits="EditorExportPlatform" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../class.xsd"> + <brief_description> + Base class for custom [EditorExportPlatform] implementations (plugins). + </brief_description> + <description> + External [EditorExportPlatform] implementations should inherit from this class. + To use [EditorExportPlatform], register it using the [method EditorPlugin.add_export_platform] method first. + </description> + <tutorials> + </tutorials> + <methods> + <method name="_can_export" qualifiers="virtual const"> + <return type="bool" /> + <param index="0" name="preset" type="EditorExportPreset" /> + <param index="1" name="debug" type="bool" /> + <description> + [b]Optional.[/b] + Returns [code]true[/code], if specified [param preset] is valid and can be exported. Use [method set_config_error] and [method set_config_missing_templates] to set error details. + Usual implementation can call [method _has_valid_export_configuration] and [method _has_valid_project_configuration] to determine if export is possible. + </description> + </method> + <method name="_cleanup" qualifiers="virtual"> + <return type="void" /> + <description> + [b]Optional.[/b] + Called by the editor before platform is unregistered. + </description> + </method> + <method name="_export_pack" qualifiers="virtual"> + <return type="int" enum="Error" /> + <param index="0" name="preset" type="EditorExportPreset" /> + <param index="1" name="debug" type="bool" /> + <param index="2" name="path" type="String" /> + <param index="3" name="flags" type="int" enum="EditorExportPlatform.DebugFlags" is_bitfield="true" /> + <description> + [b]Optional.[/b] + Creates a PCK archive at [param path] for the specified [param preset]. + This method is called when "Export PCK/ZIP" button is pressed in the export dialog, and PCK is selected as a file type. + </description> + </method> + <method name="_export_project" qualifiers="virtual"> + <return type="int" enum="Error" /> + <param index="0" name="preset" type="EditorExportPreset" /> + <param index="1" name="debug" type="bool" /> + <param index="2" name="path" type="String" /> + <param index="3" name="flags" type="int" enum="EditorExportPlatform.DebugFlags" is_bitfield="true" /> + <description> + [b]Required.[/b] + Creates a full project at [param path] for the specified [param preset]. + This method is called when "Export" button is pressed in the export dialog. + This method implementation can call [method EditorExportPlatform.save_pack] or [method EditorExportPlatform.save_zip] to use default PCK/ZIP export process, or calls [method EditorExportPlatform.export_project_files] and implement custom callback for processing each exported file. + </description> + </method> + <method name="_export_zip" qualifiers="virtual"> + <return type="int" enum="Error" /> + <param index="0" name="preset" type="EditorExportPreset" /> + <param index="1" name="debug" type="bool" /> + <param index="2" name="path" type="String" /> + <param index="3" name="flags" type="int" enum="EditorExportPlatform.DebugFlags" is_bitfield="true" /> + <description> + [b]Optional.[/b] + Create a ZIP archive at [param path] for the specified [param preset]. + This method is called when "Export PCK/ZIP" button is pressed in the export dialog, and ZIP is selected as a file type. + </description> + </method> + <method name="_get_binary_extensions" qualifiers="virtual const"> + <return type="PackedStringArray" /> + <param index="0" name="preset" type="EditorExportPreset" /> + <description> + [b]Required.[/b] + Returns array of supported binary extensions for the full project export. + </description> + </method> + <method name="_get_debug_protocol" qualifiers="virtual const"> + <return type="String" /> + <description> + [b]Optional.[/b] + Returns protocol used for remote debugging. Default implementation return [code]tcp://[/code]. + </description> + </method> + <method name="_get_device_architecture" qualifiers="virtual const"> + <return type="String" /> + <param index="0" name="device" type="int" /> + <description> + [b]Optional.[/b] + Returns device architecture for one-click deploy. + </description> + </method> + <method name="_get_export_option_visibility" qualifiers="virtual const"> + <return type="bool" /> + <param index="0" name="preset" type="EditorExportPreset" /> + <param index="1" name="option" type="String" /> + <description> + [b]Optional.[/b] + Validates [param option] and returns visibility for the specified [param preset]. Default implementation return [code]true[/code] for all options. + </description> + </method> + <method name="_get_export_option_warning" qualifiers="virtual const"> + <return type="String" /> + <param index="0" name="preset" type="EditorExportPreset" /> + <param index="1" name="option" type="StringName" /> + <description> + [b]Optional.[/b] + Validates [param option] and returns warning message for the specified [param preset]. Default implementation return empty string for all options. + </description> + </method> + <method name="_get_export_options" qualifiers="virtual const"> + <return type="Dictionary[]" /> + <description> + [b]Optional.[/b] + Returns a property list, as an [Array] of dictionaries. Each [Dictionary] must at least contain the [code]name: StringName[/code] and [code]type: Variant.Type[/code] entries. + Additionally, the following keys are supported: + - [code]hint: PropertyHint[/code] + - [code]hint_string: String[/code] + - [code]usage: PropertyUsageFlags[/code] + - [code]class_name: StringName[/code] + - [code]default_value: Variant[/code], default value of the property. + - [code]update_visibility: bool[/code], if set to [code]true[/code], [method _get_export_option_visibility] is called for each property when this property is changed. + - [code]required: bool[/code], if set to [code]true[/code], this property warnings are critical, and should be resolved to make export possible. This value is a hint for the [method _has_valid_export_configuration] implementation, and not used by the engine directly. + See also [method Object._get_property_list]. + </description> + </method> + <method name="_get_logo" qualifiers="virtual const"> + <return type="Texture2D" /> + <description> + [b]Required.[/b] + Returns platform logo displayed in the export dialog, logo should be 32x32 adjusted to the current editor scale, see [method EditorInterface.get_editor_scale]. + </description> + </method> + <method name="_get_name" qualifiers="virtual const"> + <return type="String" /> + <description> + [b]Required.[/b] + Returns export platform name. + </description> + </method> + <method name="_get_option_icon" qualifiers="virtual const"> + <return type="ImageTexture" /> + <param index="0" name="device" type="int" /> + <description> + [b]Optional.[/b] + Returns one-click deploy menu item icon for the specified [param device], icon should be 16x16 adjusted to the current editor scale, see [method EditorInterface.get_editor_scale]. + </description> + </method> + <method name="_get_option_label" qualifiers="virtual const"> + <return type="String" /> + <param index="0" name="device" type="int" /> + <description> + [b]Optional.[/b] + Returns one-click deploy menu item label for the specified [param device]. + </description> + </method> + <method name="_get_option_tooltip" qualifiers="virtual const"> + <return type="String" /> + <param index="0" name="device" type="int" /> + <description> + [b]Optional.[/b] + Returns one-click deploy menu item tooltip for the specified [param device]. + </description> + </method> + <method name="_get_options_count" qualifiers="virtual const"> + <return type="int" /> + <description> + [b]Optional.[/b] + Returns number one-click deploy devices (or other one-click option displayed in the menu). + </description> + </method> + <method name="_get_options_tooltip" qualifiers="virtual const"> + <return type="String" /> + <description> + [b]Optional.[/b] + Returns tooltip of the one-click deploy menu button. + </description> + </method> + <method name="_get_os_name" qualifiers="virtual const"> + <return type="String" /> + <description> + [b]Required.[/b] + Returns target OS name. + </description> + </method> + <method name="_get_platform_features" qualifiers="virtual const"> + <return type="PackedStringArray" /> + <description> + [b]Required.[/b] + Returns array of platform specific features. + </description> + </method> + <method name="_get_preset_features" qualifiers="virtual const"> + <return type="PackedStringArray" /> + <param index="0" name="preset" type="EditorExportPreset" /> + <description> + [b]Required.[/b] + Returns array of platform specific features for the specified [param preset]. + </description> + </method> + <method name="_get_run_icon" qualifiers="virtual const"> + <return type="Texture2D" /> + <description> + [b]Optional.[/b] + Returns icon of the one-click deploy menu button, icon should be 16x16 adjusted to the current editor scale, see [method EditorInterface.get_editor_scale]. + </description> + </method> + <method name="_has_valid_export_configuration" qualifiers="virtual const"> + <return type="bool" /> + <param index="0" name="preset" type="EditorExportPreset" /> + <param index="1" name="debug" type="bool" /> + <description> + [b]Required.[/b] + Returns [code]true[/code] if export configuration is valid. + </description> + </method> + <method name="_has_valid_project_configuration" qualifiers="virtual const"> + <return type="bool" /> + <param index="0" name="preset" type="EditorExportPreset" /> + <description> + [b]Required.[/b] + Returns [code]true[/code] if project configuration is valid. + </description> + </method> + <method name="_is_executable" qualifiers="virtual const"> + <return type="bool" /> + <param index="0" name="path" type="String" /> + <description> + [b]Optional.[/b] + Returns [code]true[/code] if specified file is a valid executable (native executable or script) for the target platform. + </description> + </method> + <method name="_poll_export" qualifiers="virtual"> + <return type="bool" /> + <description> + [b]Optional.[/b] + Returns [code]true[/code] if one-click deploy options are changed and editor interface should be updated. + </description> + </method> + <method name="_run" qualifiers="virtual"> + <return type="int" enum="Error" /> + <param index="0" name="preset" type="EditorExportPreset" /> + <param index="1" name="device" type="int" /> + <param index="2" name="debug_flags" type="int" enum="EditorExportPlatform.DebugFlags" is_bitfield="true" /> + <description> + [b]Optional.[/b] + This method is called when [param device] one-click deploy menu option is selected. + Implementation should export project to a temporary location, upload and run it on the specific [param device], or perform another action associated with the menu item. + </description> + </method> + <method name="_should_update_export_options" qualifiers="virtual"> + <return type="bool" /> + <description> + [b]Optional.[/b] + Returns [code]true[/code] if export options list is changed and presets should be updated. + </description> + </method> + <method name="get_config_error" qualifiers="const"> + <return type="String" /> + <description> + Returns current configuration error message text. This method should be called only from the [method _can_export], [method _has_valid_export_configuration], or [method _has_valid_project_configuration] implementations. + </description> + </method> + <method name="get_config_missing_templates" qualifiers="const"> + <return type="bool" /> + <description> + Returns [code]true[/code] is export templates are missing from the current configuration. This method should be called only from the [method _can_export], [method _has_valid_export_configuration], or [method _has_valid_project_configuration] implementations. + </description> + </method> + <method name="set_config_error" qualifiers="const"> + <return type="void" /> + <param index="0" name="error_text" type="String" /> + <description> + Sets current configuration error message text. This method should be called only from the [method _can_export], [method _has_valid_export_configuration], or [method _has_valid_project_configuration] implementations. + </description> + </method> + <method name="set_config_missing_templates" qualifiers="const"> + <return type="void" /> + <param index="0" name="missing_templates" type="bool" /> + <description> + Set to [code]true[/code] is export templates are missing from the current configuration. This method should be called only from the [method _can_export], [method _has_valid_export_configuration], or [method _has_valid_project_configuration] implementations. + </description> + </method> + </methods> +</class> diff --git a/doc/classes/EditorExportPlugin.xml b/doc/classes/EditorExportPlugin.xml index 9ef911a68d..42e1968eb0 100644 --- a/doc/classes/EditorExportPlugin.xml +++ b/doc/classes/EditorExportPlugin.xml @@ -305,6 +305,18 @@ In case of a directory code-sign will error if you place non code object in directory. </description> </method> + <method name="get_export_platform" qualifiers="const"> + <return type="EditorExportPlatform" /> + <description> + Returns currently used export platform. + </description> + </method> + <method name="get_export_preset" qualifiers="const"> + <return type="EditorExportPreset" /> + <description> + Returns currently used export preset. + </description> + </method> <method name="get_option" qualifiers="const"> <return type="Variant" /> <param index="0" name="name" type="StringName" /> diff --git a/doc/classes/EditorExportPreset.xml b/doc/classes/EditorExportPreset.xml new file mode 100644 index 0000000000..bba79364e4 --- /dev/null +++ b/doc/classes/EditorExportPreset.xml @@ -0,0 +1,186 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<class name="EditorExportPreset" inherits="RefCounted" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../class.xsd"> + <brief_description> + Export preset configuration. + </brief_description> + <description> + Export preset configuration. Instances of [EditorExportPreset] by editor UI and intended to be used a read-only configuration passed to the [EditorExportPlatform] methods when exporting the project. + </description> + <tutorials> + </tutorials> + <methods> + <method name="are_advanced_options_enabled" qualifiers="const"> + <return type="bool" /> + <description> + Returns [code]true[/code], is "Advanced" toggle is enabled in the export dialog. + </description> + </method> + <method name="get_custom_features" qualifiers="const"> + <return type="String" /> + <description> + Returns string with a comma separated list of custom features. + </description> + </method> + <method name="get_customized_files" qualifiers="const"> + <return type="Dictionary" /> + <description> + Returns [Dictionary] of files selected in the "Resources" tab of the export dialog. Dictionary keys are file names and values are export mode - [code]"strip[/code], [code]"keep"[/code], or [code]"remove"[/code]. See also [method get_file_export_mode]. + </description> + </method> + <method name="get_customized_files_count" qualifiers="const"> + <return type="int" /> + <description> + Returns number of files selected in the "Resources" tab of the export dialog. + </description> + </method> + <method name="get_encrypt_directory" qualifiers="const"> + <return type="bool" /> + <description> + Returns [code]true[/code], PCK directory encryption is enabled in the export dialog. + </description> + </method> + <method name="get_encrypt_pck" qualifiers="const"> + <return type="bool" /> + <description> + Returns [code]true[/code], PCK encryption is enabled in the export dialog. + </description> + </method> + <method name="get_encryption_ex_filter" qualifiers="const"> + <return type="String" /> + <description> + Returns file filters to exclude during PCK encryption. + </description> + </method> + <method name="get_encryption_in_filter" qualifiers="const"> + <return type="String" /> + <description> + Returns file filters to include during PCK encryption. + </description> + </method> + <method name="get_encryption_key" qualifiers="const"> + <return type="String" /> + <description> + Returns PCK encryption key. + </description> + </method> + <method name="get_exclude_filter" qualifiers="const"> + <return type="String" /> + <description> + Returns file filters to exclude during export. + </description> + </method> + <method name="get_export_filter" qualifiers="const"> + <return type="int" enum="EditorExportPreset.ExportFilter" /> + <description> + Returns export file filter mode selected in the "Resources" tab of the export dialog. + </description> + </method> + <method name="get_export_path" qualifiers="const"> + <return type="String" /> + <description> + Returns export target path. + </description> + </method> + <method name="get_file_export_mode" qualifiers="const"> + <return type="int" enum="EditorExportPreset.FileExportMode" /> + <param index="0" name="path" type="String" /> + <param index="1" name="default" type="int" enum="EditorExportPreset.FileExportMode" default="0" /> + <description> + Returns file export mode for the specified file. + </description> + </method> + <method name="get_files_to_export" qualifiers="const"> + <return type="PackedStringArray" /> + <description> + Returns array of files to export. + </description> + </method> + <method name="get_include_filter" qualifiers="const"> + <return type="String" /> + <description> + Returns file filters to include during export. + </description> + </method> + <method name="get_or_env" qualifiers="const"> + <return type="Variant" /> + <param index="0" name="name" type="StringName" /> + <param index="1" name="env_var" type="String" /> + <description> + Returns export option value or value of environment variable if it is set. + </description> + </method> + <method name="get_preset_name" qualifiers="const"> + <return type="String" /> + <description> + Returns export preset name. + </description> + </method> + <method name="get_script_export_mode" qualifiers="const"> + <return type="int" /> + <description> + Returns script export mode. + </description> + </method> + <method name="get_version" qualifiers="const"> + <return type="String" /> + <param index="0" name="name" type="StringName" /> + <param index="1" name="windows_version" type="bool" /> + <description> + Returns the preset's version number, or fall back to the [member ProjectSettings.application/config/version] project setting if set to an empty string. + If [param windows_version] is [code]true[/code], formats the returned version number to be compatible with Windows executable metadata. + </description> + </method> + <method name="has" qualifiers="const"> + <return type="bool" /> + <param index="0" name="property" type="StringName" /> + <description> + Returns [code]true[/code] if preset has specified property. + </description> + </method> + <method name="has_export_file"> + <return type="bool" /> + <param index="0" name="path" type="String" /> + <description> + Returns [code]true[/code] if specified file is exported. + </description> + </method> + <method name="is_dedicated_server" qualifiers="const"> + <return type="bool" /> + <description> + Returns [code]true[/code] if dedicated server export mode is selected in the export dialog. + </description> + </method> + <method name="is_runnable" qualifiers="const"> + <return type="bool" /> + <description> + Returns [code]true[/code] if "Runnable" toggle is enabled in the export dialog. + </description> + </method> + </methods> + <constants> + <constant name="EXPORT_ALL_RESOURCES" value="0" enum="ExportFilter"> + </constant> + <constant name="EXPORT_SELECTED_SCENES" value="1" enum="ExportFilter"> + </constant> + <constant name="EXPORT_SELECTED_RESOURCES" value="2" enum="ExportFilter"> + </constant> + <constant name="EXCLUDE_SELECTED_RESOURCES" value="3" enum="ExportFilter"> + </constant> + <constant name="EXPORT_CUSTOMIZED" value="4" enum="ExportFilter"> + </constant> + <constant name="MODE_FILE_NOT_CUSTOMIZED" value="0" enum="FileExportMode"> + </constant> + <constant name="MODE_FILE_STRIP" value="1" enum="FileExportMode"> + </constant> + <constant name="MODE_FILE_KEEP" value="2" enum="FileExportMode"> + </constant> + <constant name="MODE_FILE_REMOVE" value="3" enum="FileExportMode"> + </constant> + <constant name="MODE_SCRIPT_TEXT" value="0" enum="ScriptExportMode"> + </constant> + <constant name="MODE_SCRIPT_BINARY_TOKENS" value="1" enum="ScriptExportMode"> + </constant> + <constant name="MODE_SCRIPT_BINARY_TOKENS_COMPRESSED" value="2" enum="ScriptExportMode"> + </constant> + </constants> +</class> diff --git a/doc/classes/EditorInterface.xml b/doc/classes/EditorInterface.xml index 7187617c4c..795c5c1c2f 100644 --- a/doc/classes/EditorInterface.xml +++ b/doc/classes/EditorInterface.xml @@ -117,6 +117,12 @@ [b]Note:[/b] When creating custom editor UI, prefer accessing theme items directly from your GUI nodes using the [code]get_theme_*[/code] methods. </description> </method> + <method name="get_editor_undo_redo" qualifiers="const"> + <return type="EditorUndoRedoManager" /> + <description> + Returns the editor's [EditorUndoRedoManager]. + </description> + </method> <method name="get_editor_viewport_2d" qualifiers="const"> <return type="SubViewport" /> <description> @@ -299,9 +305,10 @@ <return type="void" /> <param index="0" name="callback" type="Callable" /> <param index="1" name="valid_types" type="StringName[]" default="[]" /> + <param index="2" name="current_value" type="Node" default="null" /> <description> - Pops up an editor dialog for selecting a [Node] from the edited scene. The [param callback] must take a single argument of type [NodePath]. It is called on the selected [NodePath] or the empty path [code]^""[/code] if the dialog is canceled. If [param valid_types] is provided, the dialog will only show Nodes that match one of the listed Node types. - [b]Example:[/b] + Pops up an editor dialog for selecting a [Node] from the edited scene. The [param callback] must take a single argument of type [NodePath]. It is called on the selected [NodePath] or the empty path [code]^""[/code] if the dialog is canceled. If [param valid_types] is provided, the dialog will only show Nodes that match one of the listed Node types. If [param current_value] is provided, the Node will be automatically selected in the tree, if it exists. + [b]Example:[/b] Display the node selection dialog as soon as this node is added to the tree for the first time: [codeblock] func _ready(): if Engine.is_editor_hint(): @@ -320,9 +327,9 @@ <param index="0" name="object" type="Object" /> <param index="1" name="callback" type="Callable" /> <param index="2" name="type_filter" type="PackedInt32Array" default="PackedInt32Array()" /> + <param index="3" name="current_value" type="String" default="""" /> <description> - Pops up an editor dialog for selecting properties from [param object]. The [param callback] must take a single argument of type [NodePath]. It is called on the selected property path (see [method NodePath.get_as_property_path]) or the empty path [code]^""[/code] if the dialog is canceled. If [param type_filter] is provided, the dialog will only show properties that match one of the listed [enum Variant.Type] values. - [b]Example:[/b] + Pops up an editor dialog for selecting properties from [param object]. The [param callback] must take a single argument of type [NodePath]. It is called on the selected property path (see [method NodePath.get_as_property_path]) or the empty path [code]^""[/code] if the dialog is canceled. If [param type_filter] is provided, the dialog will only show properties that match one of the listed [enum Variant.Type] values. If [param current_value] is provided, the property will be selected automatically in the property list, if it exists. [codeblock] func _ready(): if Engine.is_editor_hint(): diff --git a/doc/classes/EditorPlugin.xml b/doc/classes/EditorPlugin.xml index 9c1a6f6af6..37f8b2213b 100644 --- a/doc/classes/EditorPlugin.xml +++ b/doc/classes/EditorPlugin.xml @@ -104,7 +104,6 @@ <param index="1" name="event" type="InputEvent" /> <description> Called when there is a root node in the current edited scene, [method _handles] is implemented, and an [InputEvent] happens in the 3D viewport. The return value decides whether the [InputEvent] is consumed or forwarded to other [EditorPlugin]s. See [enum AfterGUIInput] for options. - [b]Example:[/b] [codeblocks] [gdscript] # Prevents the InputEvent from reaching other Editor classes. @@ -119,8 +118,7 @@ } [/csharp] [/codeblocks] - Must [code]return EditorPlugin.AFTER_GUI_INPUT_PASS[/code] in order to forward the [InputEvent] to other Editor classes. - [b]Example:[/b] + This method must return [constant AFTER_GUI_INPUT_PASS] in order to forward the [InputEvent] to other Editor classes. [codeblocks] [gdscript] # Consumes InputEventMouseMotion and forwards other InputEvent types. @@ -188,8 +186,7 @@ <return type="bool" /> <param index="0" name="event" type="InputEvent" /> <description> - Called when there is a root node in the current edited scene, [method _handles] is implemented and an [InputEvent] happens in the 2D viewport. Intercepts the [InputEvent], if [code]return true[/code] [EditorPlugin] consumes the [param event], otherwise forwards [param event] to other Editor classes. - [b]Example:[/b] + Called when there is a root node in the current edited scene, [method _handles] is implemented, and an [InputEvent] happens in the 2D viewport. If this method returns [code]true[/code], [param event] is intercepted by this [EditorPlugin], otherwise [param event] is forwarded to other Editor classes. [codeblocks] [gdscript] # Prevents the InputEvent from reaching other Editor classes. @@ -204,8 +201,7 @@ } [/csharp] [/codeblocks] - Must [code]return false[/code] in order to forward the [InputEvent] to other Editor classes. - [b]Example:[/b] + This method must return [code]false[/code] in order to forward the [InputEvent] to other Editor classes. [codeblocks] [gdscript] # Consumes InputEventMouseMotion and forwards other InputEvent types. @@ -459,6 +455,13 @@ Adds a [Script] as debugger plugin to the Debugger. The script must extend [EditorDebuggerPlugin]. </description> </method> + <method name="add_export_platform"> + <return type="void" /> + <param index="0" name="platform" type="EditorExportPlatform" /> + <description> + Registers a new [EditorExportPlatform]. Export platforms provides functionality of exporting to the specific platform. + </description> + </method> <method name="add_export_plugin"> <return type="void" /> <param index="0" name="plugin" type="EditorExportPlugin" /> @@ -657,6 +660,13 @@ Removes the debugger plugin with given script from the Debugger. </description> </method> + <method name="remove_export_platform"> + <return type="void" /> + <param index="0" name="platform" type="EditorExportPlatform" /> + <description> + Removes an export platform registered by [method add_export_platform]. + </description> + </method> <method name="remove_export_plugin"> <return type="void" /> <param index="0" name="plugin" type="EditorExportPlugin" /> diff --git a/doc/classes/EditorResourcePreviewGenerator.xml b/doc/classes/EditorResourcePreviewGenerator.xml index fcfdbb5c44..9c9b6d11b2 100644 --- a/doc/classes/EditorResourcePreviewGenerator.xml +++ b/doc/classes/EditorResourcePreviewGenerator.xml @@ -23,7 +23,7 @@ <param index="2" name="metadata" type="Dictionary" /> <description> Generate a preview from a given resource with the specified size. This must always be implemented. - Returning an empty texture is an OK way to fail and let another generator take care. + Returning [code]null[/code] is an OK way to fail and let another generator take care. Care must be taken because this function is always called from a thread (not the main thread). [param metadata] dictionary can be modified to store file-specific metadata that can be used in [method EditorResourceTooltipPlugin._make_tooltip_for_path] (like image size, sample length etc.). </description> @@ -35,7 +35,7 @@ <param index="2" name="metadata" type="Dictionary" /> <description> Generate a preview directly from a path with the specified size. Implementing this is optional, as default code will load and call [method _generate]. - Returning an empty texture is an OK way to fail and let another generator take care. + Returning [code]null[/code] is an OK way to fail and let another generator take care. Care must be taken because this function is always called from a thread (not the main thread). [param metadata] dictionary can be modified to store file-specific metadata that can be used in [method EditorResourceTooltipPlugin._make_tooltip_for_path] (like image size, sample length etc.). </description> diff --git a/doc/classes/EditorSettings.xml b/doc/classes/EditorSettings.xml index 320b119b6a..b6565b81f2 100644 --- a/doc/classes/EditorSettings.xml +++ b/doc/classes/EditorSettings.xml @@ -38,7 +38,6 @@ - [code]name[/code]: [String] (the name of the property) - [code]type[/code]: [int] (see [enum Variant.Type]) - optionally [code]hint[/code]: [int] (see [enum PropertyHint]) and [code]hint_string[/code]: [String] - [b]Example:[/b] [codeblocks] [gdscript] var settings = EditorInterface.get_editor_settings() @@ -259,11 +258,14 @@ The color to use when drawing smart snapping lines in the 2D editor. The smart snapping lines will automatically display when moving 2D nodes if smart snapping is enabled in the Snapping Options menu at the top of the 2D editor viewport. </member> <member name="editors/2d/use_integer_zoom_by_default" type="bool" setter="" getter=""> - If [code]true[/code], the 2D editor will snap to integer zoom values while not holding the [kbd]Alt[/kbd] key and powers of two while holding it. If [code]false[/code], this behavior is swapped. + If [code]true[/code], the 2D editor will snap to integer zoom values when not holding the [kbd]Alt[/kbd] key. If [code]false[/code], this behavior is swapped. </member> <member name="editors/2d/viewport_border_color" type="Color" setter="" getter=""> The color of the viewport border in the 2D editor. This border represents the viewport's size at the base resolution defined in the Project Settings. Objects placed outside this border will not be visible unless a [Camera2D] node is used, or unless the window is resized and the stretch mode is set to [code]disabled[/code]. </member> + <member name="editors/2d/zoom_speed_factor" type="float" setter="" getter=""> + The factor to use when zooming in or out in the 2D editor. For example, [code]1.1[/code] will zoom in by 10% with every step. If set to [code]2.0[/code], zooming will only cycle through powers of two. + </member> <member name="editors/3d/default_fov" type="float" setter="" getter=""> The default camera vertical field of view to use in the 3D editor (in degrees). The camera field of view can be adjusted on a per-scene basis using the [b]View[/b] menu at the top of the 3D editor. If a scene had its camera field of view adjusted using the [b]View[/b] menu, this setting is ignored in the scene in question. This setting is also ignored while a [Camera3D] node is being previewed in the editor. [b]Note:[/b] The editor camera always uses the [b]Keep Height[/b] aspect mode. @@ -322,7 +324,6 @@ <member name="editors/3d/navigation/emulate_3_button_mouse" type="bool" setter="" getter=""> If [code]true[/code], enables 3-button mouse emulation mode. This is useful on laptops when using a trackpad. When 3-button mouse emulation mode is enabled, the pan, zoom and orbit modifiers can always be used in the 3D editor viewport, even when not holding down any mouse button. - [b]Note:[/b] No matter the orbit modifier configured in [member editors/3d/navigation/orbit_modifier], [kbd]Alt[/kbd] will always remain usable for orbiting in this mode to improve usability with graphics tablets. </member> <member name="editors/3d/navigation/emulate_numpad" type="bool" setter="" getter=""> If [code]true[/code], allows using the top row [kbd]0[/kbd]-[kbd]9[/kbd] keys to function as their equivalent numpad keys for 3D editor navigation. This should be enabled on keyboards that have no numeric keypad available. @@ -334,28 +335,25 @@ If [code]true[/code], invert the vertical mouse axis when panning, orbiting, or using freelook mode in the 3D editor. </member> <member name="editors/3d/navigation/navigation_scheme" type="int" setter="" getter=""> - The navigation scheme to use in the 3D editor. Changing this setting will affect the mouse buttons that must be held down to perform certain operations in the 3D editor viewport. - - [b]Godot[/b] Middle mouse button to orbit, [kbd]Shift + Middle mouse button[/kbd] to pan. [kbd]Mouse wheel[/kbd] to zoom. - - [b]Maya:[/b] [kbd]Alt + Left mouse button[/kbd] to orbit. [kbd]Middle mouse button[/kbd] to pan, [kbd]Shift + Middle mouse button[/kbd] to pan 10 times faster. [kbd]Mouse wheel[/kbd] to zoom. + The navigation scheme preset to use in the 3D editor. Changing this setting will affect the mouse button and modifier controls used to navigate the 3D editor viewport. + All schemes can use [kbd]Mouse wheel[/kbd] to zoom. + - [b]Godot:[/b] [kbd]Middle mouse button[/kbd] to orbit. [kbd]Shift + Middle mouse button[/kbd] to pan. [kbd]Ctrl + Shift + Middle mouse button[/kbd] to zoom. + - [b]Maya:[/b] [kbd]Alt + Left mouse button[/kbd] to orbit. [kbd]Middle mouse button[/kbd] to pan, [kbd]Shift + Middle mouse button[/kbd] to pan 10 times faster. [kbd]Alt + Right mouse button[/kbd] to zoom. - [b]Modo:[/b] [kbd]Alt + Left mouse button[/kbd] to orbit. [kbd]Alt + Shift + Left mouse button[/kbd] to pan. [kbd]Ctrl + Alt + Left mouse button[/kbd] to zoom. - See also [member editors/3d/freelook/freelook_navigation_scheme]. + See also [member editors/3d/navigation/orbit_mouse_button], [member editors/3d/navigation/pan_mouse_button], [member editors/3d/navigation/zoom_mouse_button], and [member editors/3d/freelook/freelook_navigation_scheme]. [b]Note:[/b] On certain window managers on Linux, the [kbd]Alt[/kbd] key will be intercepted by the window manager when clicking a mouse button at the same time. This means Godot will not see the modifier key as being pressed. </member> - <member name="editors/3d/navigation/orbit_modifier" type="int" setter="" getter=""> - The modifier key that must be held to orbit in the 3D editor. - [b]Note:[/b] If [member editors/3d/navigation/emulate_3_button_mouse] is [code]true[/code], [kbd]Alt[/kbd] will always remain usable for orbiting to improve usability with graphics tablets. - [b]Note:[/b] On certain window managers on Linux, the [kbd]Alt[/kbd] key will be intercepted by the window manager when clicking a mouse button at the same time. This means Godot will not see the modifier key as being pressed. + <member name="editors/3d/navigation/orbit_mouse_button" type="int" setter="" getter=""> + The mouse button that needs to be held down to orbit in the 3D editor viewport. </member> - <member name="editors/3d/navigation/pan_modifier" type="int" setter="" getter=""> - The modifier key that must be held to pan in the 3D editor. - [b]Note:[/b] On certain window managers on Linux, the [kbd]Alt[/kbd] key will be intercepted by the window manager when clicking a mouse button at the same time. This means Godot will not see the modifier key as being pressed. + <member name="editors/3d/navigation/pan_mouse_button" type="int" setter="" getter=""> + The mouse button that needs to be held down to pan in the 3D editor viewport. </member> <member name="editors/3d/navigation/warped_mouse_panning" type="bool" setter="" getter=""> If [code]true[/code], warps the mouse around the 3D viewport while panning in the 3D editor. This makes it possible to pan over a large area without having to exit panning and adjust the mouse cursor. </member> - <member name="editors/3d/navigation/zoom_modifier" type="int" setter="" getter=""> - The modifier key that must be held to zoom in the 3D editor. - [b]Note:[/b] On certain window managers on Linux, the [kbd]Alt[/kbd] key will be intercepted by the window manager when clicking a mouse button at the same time. This means Godot will not see the modifier key as being pressed. + <member name="editors/3d/navigation/zoom_mouse_button" type="int" setter="" getter=""> + The mouse button that needs to be held down to zoom in the 3D editor viewport. </member> <member name="editors/3d/navigation/zoom_style" type="int" setter="" getter=""> The mouse cursor movement direction to use when zooming by moving the mouse. This does not affect zooming with the mouse wheel. @@ -678,6 +676,9 @@ <member name="interface/editor/import_resources_when_unfocused" type="bool" setter="" getter=""> If [code]true[/code], (re)imports resources even if the editor window is unfocused or minimized. If [code]false[/code], resources are only (re)imported when the editor window is focused. This can be set to [code]true[/code] to speed up iteration by starting the import process earlier when saving files in the project folder. This also allows getting visual feedback on changes without having to click the editor window, which is useful with multi-monitor setups. The downside of setting this to [code]true[/code] is that it increases idle CPU usage and may steal CPU time from other applications when importing resources. </member> + <member name="interface/editor/keep_screen_on" type="bool" setter="" getter=""> + If [code]true[/code], keeps the screen on (even in case of inactivity), so the screensaver does not take over. Works on desktop and mobile platforms. + </member> <member name="interface/editor/localize_settings" type="bool" setter="" getter=""> If [code]true[/code], setting names in the editor are localized when possible. [b]Note:[/b] This setting affects most [EditorInspector]s in the editor UI, primarily Project Settings and Editor Settings. To control names displayed in the Inspector dock, use [member interface/inspector/default_property_name_style] instead. @@ -701,6 +702,9 @@ <member name="interface/editor/project_manager_screen" type="int" setter="" getter=""> The preferred monitor to display the project manager. </member> + <member name="interface/editor/remember_window_size_and_position" type="bool" setter="" getter=""> + If [code]true[/code], the editor window will remember its size, position, and which screen it was displayed on across restarts. + </member> <member name="interface/editor/save_each_scene_on_quit" type="bool" setter="" getter=""> If [code]false[/code], the editor will save all scenes when confirming the [b]Save[/b] action when quitting the editor or quitting to the project list. If [code]true[/code], the editor will ask to save each scene individually. </member> @@ -959,7 +963,17 @@ If [code]true[/code], on Linux/BSD, the editor will check for Wayland first instead of X11 (if available). </member> <member name="run/window_placement/android_window" type="int" setter="" getter=""> - The Android window to display the project on when starting the project from the editor. + Specifies how the Play window is launched relative to the Android editor. + - [b]Auto (based on screen size)[/b] (default) will automatically choose how to launch the Play window based on the device and screen metrics. Defaults to [b]Same as Editor[/b] on phones and [b]Side-by-side with Editor[/b] on tablets. + - [b]Same as Editor[/b] will launch the Play window in the same window as the Editor. + - [b]Side-by-side with Editor[/b] will launch the Play window side-by-side with the Editor window. + [b]Note:[/b] Only available in the Android editor. + </member> + <member name="run/window_placement/play_window_pip_mode" type="int" setter="" getter=""> + Specifies the picture-in-picture (PiP) mode for the Play window. + - [b]Disabled:[/b] PiP is disabled for the Play window. + - [b]Enabled:[/b] If the device supports it, PiP is always enabled for the Play window. The Play window will contain a button to enter PiP mode. + - [b]Enabled when Play window is same as Editor[/b] (default for Android editor): If the device supports it, PiP is enabled when the Play window is the same as the Editor. The Play window will contain a button to enter PiP mode. [b]Note:[/b] Only available in the Android editor. </member> <member name="run/window_placement/rect" type="int" setter="" getter=""> diff --git a/doc/classes/EditorSpinSlider.xml b/doc/classes/EditorSpinSlider.xml index 783f1243e2..83c65b736e 100644 --- a/doc/classes/EditorSpinSlider.xml +++ b/doc/classes/EditorSpinSlider.xml @@ -51,4 +51,12 @@ </description> </signal> </signals> + <theme_items> + <theme_item name="updown" data_type="icon" type="Texture2D"> + Single texture representing both the up and down buttons. + </theme_item> + <theme_item name="updown_disabled" data_type="icon" type="Texture2D"> + Single texture representing both the up and down buttons, when the control is readonly or disabled. + </theme_item> + </theme_items> </class> diff --git a/doc/classes/EditorUndoRedoManager.xml b/doc/classes/EditorUndoRedoManager.xml index 5ac0d790a2..0f8c69a1bb 100644 --- a/doc/classes/EditorUndoRedoManager.xml +++ b/doc/classes/EditorUndoRedoManager.xml @@ -68,6 +68,21 @@ Register a reference for "undo" that will be erased if the "undo" history is lost. This is useful mostly for nodes removed with the "do" call (not the "undo" call!). </description> </method> + <method name="clear_history"> + <return type="void" /> + <param index="0" name="id" type="int" default="-99" /> + <param index="1" name="increase_version" type="bool" default="true" /> + <description> + Clears the given undo history. You can clear history for a specific scene, global history, or for all scenes at once if [param id] is [constant INVALID_HISTORY]. + If [param increase_version] is [code]true[/code], the undo history version will be increased, marking it as unsaved. Useful for operations that modify the scene, but don't support undo. + [codeblock] + var scene_root = EditorInterface.get_edited_scene_root() + var undo_redo = EditorInterface.get_editor_undo_redo() + undo_redo.clear_history(undo_redo.get_object_history_id(scene_root)) + [/codeblock] + [b]Note:[/b] If you want to mark an edited scene as unsaved without clearing its history, use [method EditorInterface.mark_scene_as_unsaved] instead. + </description> + </method> <method name="commit_action"> <return type="void" /> <param index="0" name="execute" type="bool" default="true" /> diff --git a/doc/classes/GDExtensionManager.xml b/doc/classes/GDExtensionManager.xml index 211bc023c0..97d2d08752 100644 --- a/doc/classes/GDExtensionManager.xml +++ b/doc/classes/GDExtensionManager.xml @@ -56,6 +56,20 @@ </method> </methods> <signals> + <signal name="extension_loaded"> + <param index="0" name="extension" type="GDExtension" /> + <description> + Emitted after the editor has finished loading a new extension. + [b]Note:[/b] This signal is only emitted in editor builds. + </description> + </signal> + <signal name="extension_unloading"> + <param index="0" name="extension" type="GDExtension" /> + <description> + Emitted before the editor starts unloading an extension. + [b]Note:[/b] This signal is only emitted in editor builds. + </description> + </signal> <signal name="extensions_reloaded"> <description> Emitted after the editor has finished reloading one or more extensions. diff --git a/doc/classes/HTTPClient.xml b/doc/classes/HTTPClient.xml index b6007a3b6b..864c29a2b5 100644 --- a/doc/classes/HTTPClient.xml +++ b/doc/classes/HTTPClient.xml @@ -59,8 +59,7 @@ <method name="get_response_headers_as_dictionary"> <return type="Dictionary" /> <description> - Returns all response headers as a Dictionary of structure [code]{ "key": "value1; value2" }[/code] where the case-sensitivity of the keys and values is kept like the server delivers it. A value is a simple String, this string can have more than one value where "; " is used as separator. - [b]Example:[/b] + Returns all response headers as a [Dictionary]. Each entry is composed by the header name, and a [String] containing the values separated by [code]"; "[/code]. The casing is kept the same as the headers were received. [codeblock] { "content-length": 12, diff --git a/doc/classes/Image.xml b/doc/classes/Image.xml index e254fd56e9..0fd84fb452 100644 --- a/doc/classes/Image.xml +++ b/doc/classes/Image.xml @@ -541,7 +541,6 @@ <param index="2" name="color" type="Color" /> <description> Sets the [Color] of the pixel at [code](x, y)[/code] to [param color]. - [b]Example:[/b] [codeblocks] [gdscript] var img_width = 10 @@ -567,7 +566,6 @@ <param index="1" name="color" type="Color" /> <description> Sets the [Color] of the pixel at [param point] to [param color]. - [b]Example:[/b] [codeblocks] [gdscript] var img_width = 10 diff --git a/doc/classes/ImporterMesh.xml b/doc/classes/ImporterMesh.xml index eeabcd6e12..28ee5710d9 100644 --- a/doc/classes/ImporterMesh.xml +++ b/doc/classes/ImporterMesh.xml @@ -191,8 +191,4 @@ </description> </method> </methods> - <members> - <member name="_data" type="Dictionary" setter="_set_data" getter="_get_data" default="{ "surfaces": [] }"> - </member> - </members> </class> diff --git a/doc/classes/Input.xml b/doc/classes/Input.xml index 5fdcc0b26b..6fe5b7a802 100644 --- a/doc/classes/Input.xml +++ b/doc/classes/Input.xml @@ -287,7 +287,6 @@ <param index="0" name="event" type="InputEvent" /> <description> Feeds an [InputEvent] to the game. Can be used to artificially trigger input events from code. Also generates [method Node._input] calls. - [b]Example:[/b] [codeblocks] [gdscript] var cancel_event = InputEventAction.new() diff --git a/doc/classes/InputEventMouseMotion.xml b/doc/classes/InputEventMouseMotion.xml index 98a0221fe8..bcfe5b70fd 100644 --- a/doc/classes/InputEventMouseMotion.xml +++ b/doc/classes/InputEventMouseMotion.xml @@ -6,6 +6,7 @@ <description> Stores information about a mouse or a pen motion. This includes relative position, absolute position, and velocity. See [method Node._input]. [b]Note:[/b] By default, this event is only emitted once per frame rendered at most. If you need more precise input reporting, set [member Input.use_accumulated_input] to [code]false[/code] to make events emitted as often as possible. If you use InputEventMouseMotion to draw lines, consider implementing [url=https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm]Bresenham's line algorithm[/url] as well to avoid visible gaps in lines if the user is moving the mouse quickly. + [b]Note:[/b] This event may be emitted even when the mouse hasn't moved, either by the operating system or by Godot itself. If you really need to know if the mouse has moved (e.g. to suppress displaying a tooltip), you should check that [code]relative.is_zero_approx()[/code] is [code]false[/code]. </description> <tutorials> <link title="Using InputEvent">$DOCS_URL/tutorials/inputs/inputevent.html</link> @@ -22,12 +23,13 @@ </member> <member name="relative" type="Vector2" setter="set_relative" getter="get_relative" default="Vector2(0, 0)"> The mouse position relative to the previous position (position at the last frame). - [b]Note:[/b] Since [InputEventMouseMotion] is only emitted when the mouse moves, the last event won't have a relative position of [code]Vector2(0, 0)[/code] when the user stops moving the mouse. + [b]Note:[/b] Since [InputEventMouseMotion] may only be emitted when the mouse moves, it is not possible to reliably detect when the mouse has stopped moving by checking this property. A separate, short timer may be necessary. [b]Note:[/b] [member relative] is automatically scaled according to the content scale factor, which is defined by the project's stretch mode settings. This means mouse sensitivity will appear different depending on resolution when using [member relative] in a script that handles mouse aiming with the [constant Input.MOUSE_MODE_CAPTURED] mouse mode. To avoid this, use [member screen_relative] instead. </member> <member name="screen_relative" type="Vector2" setter="set_screen_relative" getter="get_screen_relative" default="Vector2(0, 0)"> The unscaled mouse position relative to the previous position in the coordinate system of the screen (position at the last frame). - [b]Note:[/b] Since [InputEventMouseMotion] is only emitted when the mouse moves, the last event won't have a relative position of [code]Vector2(0, 0)[/code] when the user stops moving the mouse. This coordinate is [i]not[/i] scaled according to the content scale factor or calls to [method InputEvent.xformed_by]. This should be preferred over [member relative] for mouse aiming when using the [constant Input.MOUSE_MODE_CAPTURED] mouse mode, regardless of the project's stretch mode. + [b]Note:[/b] Since [InputEventMouseMotion] may only be emitted when the mouse moves, it is not possible to reliably detect when the mouse has stopped moving by checking this property. A separate, short timer may be necessary. + [b]Note:[/b] This coordinate is [i]not[/i] scaled according to the content scale factor or calls to [method InputEvent.xformed_by]. This should be preferred over [member relative] for mouse aiming when using the [constant Input.MOUSE_MODE_CAPTURED] mouse mode, regardless of the project's stretch mode. </member> <member name="screen_velocity" type="Vector2" setter="set_screen_velocity" getter="get_screen_velocity" default="Vector2(0, 0)"> The unscaled mouse velocity in pixels per second in screen coordinates. This velocity is [i]not[/i] scaled according to the content scale factor or calls to [method InputEvent.xformed_by]. This should be preferred over [member velocity] for mouse aiming when using the [constant Input.MOUSE_MODE_CAPTURED] mouse mode, regardless of the project's stretch mode. diff --git a/doc/classes/JSON.xml b/doc/classes/JSON.xml index d97a68cf2e..fe5fdfa89a 100644 --- a/doc/classes/JSON.xml +++ b/doc/classes/JSON.xml @@ -6,8 +6,7 @@ <description> The [JSON] class enables all data types to be converted to and from a JSON string. This is useful for serializing data, e.g. to save to a file or send over the network. [method stringify] is used to convert any data type into a JSON string. - [method parse] is used to convert any existing JSON data into a [Variant] that can be used within Godot. If successfully parsed, use [member data] to retrieve the [Variant], and use [code]typeof[/code] to check if the Variant's type is what you expect. JSON Objects are converted into a [Dictionary], but JSON data can be used to store [Array]s, numbers, [String]s and even just a boolean. - [b]Example[/b] + [method parse] is used to convert any existing JSON data into a [Variant] that can be used within Godot. If successfully parsed, use [member data] to retrieve the [Variant], and use [method @GlobalScope.typeof] to check if the Variant's type is what you expect. JSON Objects are converted into a [Dictionary], but JSON data can be used to store [Array]s, numbers, [String]s and even just a boolean. [codeblock] var data_to_send = ["a", "b", "c"] var json_string = JSON.stringify(data_to_send) @@ -33,11 +32,21 @@ - Trailing commas in arrays or objects are ignored, instead of causing a parser error. - New line and tab characters are accepted in string literals, and are treated like their corresponding escape sequences [code]\n[/code] and [code]\t[/code]. - Numbers are parsed using [method String.to_float] which is generally more lax than the JSON specification. - - Certain errors, such as invalid Unicode sequences, do not cause a parser error. Instead, the string is cleansed and an error is logged to the console. + - Certain errors, such as invalid Unicode sequences, do not cause a parser error. Instead, the string is cleaned up and an error is logged to the console. </description> <tutorials> </tutorials> <methods> + <method name="from_native" qualifiers="static"> + <return type="Variant" /> + <param index="0" name="variant" type="Variant" /> + <param index="1" name="allow_classes" type="bool" default="false" /> + <param index="2" name="allow_scripts" type="bool" default="false" /> + <description> + Converts a native engine type to a JSON-compliant dictionary. + By default, classes and scripts are ignored for security reasons, unless [param allow_classes] or [param allow_scripts] are specified. + </description> + </method> <method name="get_error_line" qualifiers="const"> <return type="int" /> <description> @@ -124,6 +133,16 @@ [/codeblock] </description> </method> + <method name="to_native" qualifiers="static"> + <return type="Variant" /> + <param index="0" name="json" type="Variant" /> + <param index="1" name="allow_classes" type="bool" default="false" /> + <param index="2" name="allow_scripts" type="bool" default="false" /> + <description> + Converts a JSON-compliant dictionary that was created with [method from_native] back to native engine types. + By default, classes and scripts are ignored for security reasons, unless [param allow_classes] or [param allow_scripts] are specified. + </description> + </method> </methods> <members> <member name="data" type="Variant" setter="set_data" getter="get_data" default="null"> diff --git a/doc/classes/JavaScriptObject.xml b/doc/classes/JavaScriptObject.xml index 73a06c4719..914fd997f4 100644 --- a/doc/classes/JavaScriptObject.xml +++ b/doc/classes/JavaScriptObject.xml @@ -5,7 +5,6 @@ </brief_description> <description> JavaScriptObject is used to interact with JavaScript objects retrieved or created via [method JavaScriptBridge.get_interface], [method JavaScriptBridge.create_object], or [method JavaScriptBridge.create_callback]. - [b]Example:[/b] [codeblock] extends Node diff --git a/doc/classes/Label.xml b/doc/classes/Label.xml index 8acd05cbd1..e6eba30ab7 100644 --- a/doc/classes/Label.xml +++ b/doc/classes/Label.xml @@ -59,7 +59,7 @@ Controls the text's horizontal alignment. Supports left, center, right, and fill, or justify. Set it to one of the [enum HorizontalAlignment] constants. </member> <member name="justification_flags" type="int" setter="set_justification_flags" getter="get_justification_flags" enum="TextServer.JustificationFlag" is_bitfield="true" default="163"> - Line fill alignment rules. For more info see [enum TextServer.JustificationFlag]. + Line fill alignment rules. See [enum TextServer.JustificationFlag] for more information. </member> <member name="label_settings" type="LabelSettings" setter="set_label_settings" getter="get_label_settings"> A [LabelSettings] resource that can be shared between multiple [Label] nodes. Takes priority over theme properties. diff --git a/doc/classes/Label3D.xml b/doc/classes/Label3D.xml index 4c70897452..ff26c5490d 100644 --- a/doc/classes/Label3D.xml +++ b/doc/classes/Label3D.xml @@ -73,7 +73,7 @@ Controls the text's horizontal alignment. Supports left, center, right, and fill, or justify. Set it to one of the [enum HorizontalAlignment] constants. </member> <member name="justification_flags" type="int" setter="set_justification_flags" getter="get_justification_flags" enum="TextServer.JustificationFlag" is_bitfield="true" default="163"> - Line fill alignment rules. For more info see [enum TextServer.JustificationFlag]. + Line fill alignment rules. See [enum TextServer.JustificationFlag] for more information. </member> <member name="language" type="String" setter="set_language" getter="get_language" default=""""> Language code used for line-breaking and text shaping algorithms, if left empty current locale is used instead. diff --git a/doc/classes/LineEdit.xml b/doc/classes/LineEdit.xml index 77fff22157..f938460c2f 100644 --- a/doc/classes/LineEdit.xml +++ b/doc/classes/LineEdit.xml @@ -231,8 +231,8 @@ </member> <member name="max_length" type="int" setter="set_max_length" getter="get_max_length" default="0"> Maximum number of characters that can be entered inside the [LineEdit]. If [code]0[/code], there is no limit. - When a limit is defined, characters that would exceed [member max_length] are truncated. This happens both for existing [member text] contents when setting the max length, or for new text inserted in the [LineEdit], including pasting. If any input text is truncated, the [signal text_change_rejected] signal is emitted with the truncated substring as parameter. - [b]Example:[/b] + When a limit is defined, characters that would exceed [member max_length] are truncated. This happens both for existing [member text] contents when setting the max length, or for new text inserted in the [LineEdit], including pasting. + If any input text is truncated, the [signal text_change_rejected] signal is emitted with the truncated substring as parameter: [codeblocks] [gdscript] text = "Hello world" diff --git a/doc/classes/LinkButton.xml b/doc/classes/LinkButton.xml index bcdffcd1ee..b1b3d74711 100644 --- a/doc/classes/LinkButton.xml +++ b/doc/classes/LinkButton.xml @@ -32,7 +32,6 @@ </member> <member name="uri" type="String" setter="set_uri" getter="get_uri" default=""""> The [url=https://en.wikipedia.org/wiki/Uniform_Resource_Identifier]URI[/url] for this [LinkButton]. If set to a valid URI, pressing the button opens the URI using the operating system's default program for the protocol (via [method OS.shell_open]). HTTP and HTTPS URLs open the default web browser. - [b]Examples:[/b] [codeblocks] [gdscript] uri = "https://godotengine.org" # Opens the URL in the default web browser. diff --git a/doc/classes/MeshDataTool.xml b/doc/classes/MeshDataTool.xml index 0b9890b2ec..f339a26e93 100644 --- a/doc/classes/MeshDataTool.xml +++ b/doc/classes/MeshDataTool.xml @@ -139,8 +139,7 @@ <param index="1" name="vertex" type="int" /> <description> Returns the specified vertex index of the given face. - Vertex argument must be either 0, 1, or 2 because faces contain three vertices. - [b]Example:[/b] + [param vertex] must be either [code]0[/code], [code]1[/code], or [code]2[/code] because faces contain three vertices. [codeblocks] [gdscript] var index = mesh_data_tool.get_face_vertex(0, 1) # Gets the index of the second vertex of the first face. diff --git a/doc/classes/Node.xml b/doc/classes/Node.xml index c54219c056..ae1eff4220 100644 --- a/doc/classes/Node.xml +++ b/doc/classes/Node.xml @@ -255,7 +255,7 @@ <return type="Node" /> <param index="0" name="flags" type="int" default="15" /> <description> - Duplicates the node, returning a new node with all of its properties, signals and groups copied from the original. The behavior can be tweaked through the [param flags] (see [enum DuplicateFlags]). + Duplicates the node, returning a new node with all of its properties, signals, groups, and children copied from the original. The behavior can be tweaked through the [param flags] (see [enum DuplicateFlags]). [b]Note:[/b] For nodes with a [Script] attached, if [method Object._init] has been defined with required parameters, the duplicated node will not have a [Script]. </description> </method> diff --git a/doc/classes/Object.xml b/doc/classes/Object.xml index ed420f4587..0cfa3a5d4a 100644 --- a/doc/classes/Object.xml +++ b/doc/classes/Object.xml @@ -136,7 +136,7 @@ } } - private List<int> _numbers = new(); + private Godot.Collections.Array<int> _numbers = new(); public override Godot.Collections.Array<Godot.Collections.Dictionary> _GetPropertyList() { @@ -173,7 +173,7 @@ if (propertyName.StartsWith("number_")) { int index = int.Parse(propertyName.Substring("number_".Length)); - numbers[index] = value.As<int>(); + _numbers[index] = value.As<int>(); return true; } return false; diff --git a/doc/classes/PackedScene.xml b/doc/classes/PackedScene.xml index 26d8fa8d5f..415e468e21 100644 --- a/doc/classes/PackedScene.xml +++ b/doc/classes/PackedScene.xml @@ -103,12 +103,6 @@ </description> </method> </methods> - <members> - <member name="_bundled" type="Dictionary" setter="_set_bundled_scene" getter="_get_bundled_scene" default="{ "conn_count": 0, "conns": PackedInt32Array(), "editable_instances": [], "names": PackedStringArray(), "node_count": 0, "node_paths": [], "nodes": PackedInt32Array(), "variants": [], "version": 3 }"> - A dictionary representation of the scene contents. - Available keys include "names" and "variants" for resources, "node_count", "nodes", "node_paths" for nodes, "editable_instances" for paths to overridden nodes, "conn_count" and "conns" for signal connections, and "version" for the format style of the PackedScene. - </member> - </members> <constants> <constant name="GEN_EDIT_STATE_DISABLED" value="0" enum="GenEditState"> If passed to [method instantiate], blocks edits to the scene state. diff --git a/doc/classes/ParticleProcessMaterial.xml b/doc/classes/ParticleProcessMaterial.xml index 1502690b45..28c60194c8 100644 --- a/doc/classes/ParticleProcessMaterial.xml +++ b/doc/classes/ParticleProcessMaterial.xml @@ -207,6 +207,10 @@ <member name="emission_ring_axis" type="Vector3" setter="set_emission_ring_axis" getter="get_emission_ring_axis"> The axis of the ring when using the emitter [constant EMISSION_SHAPE_RING]. </member> + <member name="emission_ring_cone_angle" type="float" setter="set_emission_ring_cone_angle" getter="get_emission_ring_cone_angle"> + The angle of the cone when using the emitter [constant EMISSION_SHAPE_RING]. The default angle of 90 degrees results in a ring, while an angle of 0 degrees results in a cone. Intermediate values will result in a ring where one end is larger than the other. + [b]Note:[/b] Depending on [member emission_ring_height], the angle may be clamped if the ring's end is reached to form a perfect cone. + </member> <member name="emission_ring_height" type="float" setter="set_emission_ring_height" getter="get_emission_ring_height"> The height of the ring when using the emitter [constant EMISSION_SHAPE_RING]. </member> diff --git a/doc/classes/PortableCompressedTexture2D.xml b/doc/classes/PortableCompressedTexture2D.xml index 3fc2aa2ab9..b85fe1de5c 100644 --- a/doc/classes/PortableCompressedTexture2D.xml +++ b/doc/classes/PortableCompressedTexture2D.xml @@ -52,8 +52,6 @@ </method> </methods> <members> - <member name="_data" type="PackedByteArray" setter="_set_data" getter="_get_data" default="PackedByteArray()"> - </member> <member name="keep_compressed_buffer" type="bool" setter="set_keep_compressed_buffer" getter="is_keeping_compressed_buffer" default="false"> When running on the editor, this class will keep the source compressed data in memory. Otherwise, the source compressed data is lost after loading and the resource can't be re saved. This flag allows to keep the compressed data in memory if you intend it to persist after loading. diff --git a/doc/classes/ProjectSettings.xml b/doc/classes/ProjectSettings.xml index f2747790ea..497070fa81 100644 --- a/doc/classes/ProjectSettings.xml +++ b/doc/classes/ProjectSettings.xml @@ -23,7 +23,6 @@ - [code]"name"[/code]: [String] (the property's name) - [code]"type"[/code]: [int] (see [enum Variant.Type]) - optionally [code]"hint"[/code]: [int] (see [enum PropertyHint]) and [code]"hint_string"[/code]: [String] - [b]Example:[/b] [codeblocks] [gdscript] ProjectSettings.set("category/property_name", 0) @@ -85,7 +84,6 @@ <param index="1" name="default_value" type="Variant" default="null" /> <description> Returns the value of the setting identified by [param name]. If the setting doesn't exist and [param default_value] is specified, the value of [param default_value] is returned. Otherwise, [code]null[/code] is returned. - [b]Example:[/b] [codeblocks] [gdscript] print(ProjectSettings.get_setting("application/config/name")) @@ -104,8 +102,7 @@ <param index="0" name="name" type="StringName" /> <description> Similar to [method get_setting], but applies feature tag overrides if any exists and is valid. - [b]Example:[/b] - If the following setting override exists "application/config/name.windows", and the following code is executed: + [b]Example:[/b] If the setting override [code]"application/config/name.windows"[/code] exists, and the following code is executed on a [i]Windows[/i] operating system, the overridden setting is printed instead: [codeblocks] [gdscript] print(ProjectSettings.get_setting_with_override("application/config/name")) @@ -114,7 +111,6 @@ GD.Print(ProjectSettings.GetSettingWithOverride("application/config/name")); [/csharp] [/codeblocks] - Then the overridden setting will be returned instead if the project is running on the [i]Windows[/i] operating system. </description> </method> <method name="globalize_path" qualifiers="const"> @@ -224,7 +220,6 @@ <param index="1" name="value" type="Variant" /> <description> Sets the value of a setting. - [b]Example:[/b] [codeblocks] [gdscript] ProjectSettings.set_setting("application/config/name", "Example") @@ -533,6 +528,9 @@ <member name="debug/gdscript/warnings/integer_division" type="int" setter="" getter="" default="1"> When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when dividing an integer by another integer (the decimal part will be discarded). </member> + <member name="debug/gdscript/warnings/missing_tool" type="int" setter="" getter="" default="1"> + When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when the base class script has the [code]@tool[/code] annotation, but the current class script does not have it. + </member> <member name="debug/gdscript/warnings/narrowing_conversion" type="int" setter="" getter="" default="1"> When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when passing a floating-point value to a function that expects an integer (it will be converted and lose precision). </member> @@ -818,9 +816,6 @@ <member name="display/window/energy_saving/keep_screen_on" type="bool" setter="" getter="" default="true"> If [code]true[/code], keeps the screen on (even in case of inactivity), so the screensaver does not take over. Works on desktop and mobile platforms. </member> - <member name="display/window/energy_saving/keep_screen_on.editor_hint" type="bool" setter="" getter="" default="false"> - Editor-only override for [member display/window/energy_saving/keep_screen_on]. Does not affect running project. - </member> <member name="display/window/handheld/orientation" type="int" setter="" getter="" default="0"> The default screen orientation to use on mobile devices. See [enum DisplayServer.ScreenOrientation] for possible values. [b]Note:[/b] When set to a portrait orientation, this project setting does not flip the project resolution's width and height automatically. Instead, you have to set [member display/window/size/viewport_width] and [member display/window/size/viewport_height] accordingly. @@ -2334,7 +2329,6 @@ If [code]true[/code], the renderer will interpolate the transforms of physics objects between the last two transforms, so that smooth motion is seen even when physics ticks do not coincide with rendered frames. See also [member Node.physics_interpolation_mode] and [method Node.reset_physics_interpolation]. [b]Note:[/b] If [code]true[/code], the physics jitter fix should be disabled by setting [member physics/common/physics_jitter_fix] to [code]0.0[/code]. [b]Note:[/b] This property is only read when the project starts. To toggle physics interpolation at runtime, set [member SceneTree.physics_interpolation] instead. - [b]Note:[/b] This feature is currently only implemented in the 2D renderer. </member> <member name="physics/common/physics_jitter_fix" type="float" setter="" getter="" default="0.5"> Controls how much physics ticks are synchronized with real time. For 0 or less, the ticks are synchronized. Such values are recommended for network games, where clock synchronization matters. Higher values cause higher deviation of in-game clock and real clock, but allows smoothing out framerate jitters. The default value of 0.5 should be good enough for most; values above 2 could cause the game to react to dropped frames with a noticeable delay and are not recommended. @@ -2694,6 +2688,8 @@ <member name="rendering/limits/spatial_indexer/update_iterations_per_frame" type="int" setter="" getter="" default="10"> </member> <member name="rendering/limits/time/time_rollover_secs" type="float" setter="" getter="" default="3600"> + Maximum time (in seconds) before the [code]TIME[/code] shader built-in variable rolls over. The [code]TIME[/code] variable increments by [code]delta[/code] each frame, and when it exceeds this value, it rolls over to [code]0.0[/code]. Since large floating-point values are less precise than small floating-point values, this should be set as low as possible to maximize the precision of the [code]TIME[/code] built-in variable in shaders. This is especially important on mobile platforms where precision in shaders is significantly reduced. However, if this is set too low, shader animations may appear to restart from the beginning while the project is running. + On desktop platforms, values below [code]4096[/code] are recommended, ideally below [code]2048[/code]. On mobile platforms, values below [code]64[/code] are recommended, ideally below [code]32[/code]. </member> <member name="rendering/mesh_lod/lod_change/threshold_pixels" type="float" setter="" getter="" default="1.0"> The automatic LOD bias to use for meshes rendered within the [ReflectionProbe]. Higher values will use less detailed versions of meshes that have LOD variations generated. If set to [code]0.0[/code], automatic LOD is disabled. Increase [member rendering/mesh_lod/lod_change/threshold_pixels] to improve performance at the cost of geometry detail. @@ -2954,6 +2950,12 @@ <member name="xr/openxr/environment_blend_mode" type="int" setter="" getter="" default=""0""> Specify how OpenXR should blend in the environment. This is specific to certain AR and passthrough devices where camera images are blended in by the XR compositor. </member> + <member name="xr/openxr/extensions/debug_message_types" type="int" setter="" getter="" default=""15""> + Specifies the message types for which we request debug messages. Requires [member xr/openxr/extensions/debug_utils] to be set and the extension to be supported by the XR runtime. + </member> + <member name="xr/openxr/extensions/debug_utils" type="int" setter="" getter="" default=""0""> + Enables debug utilities on XR runtimes that supports the debug utils extension. Sets the maximum severity being reported (0 = disabled, 1 = error, 2 = warning, 3 = info, 4 = verbose). + </member> <member name="xr/openxr/extensions/eye_gaze_interaction" type="bool" setter="" getter="" default="false"> Specify whether to enable eye tracking for this project. Depending on the platform, additional export configuration may be needed. </member> diff --git a/doc/classes/PropertyTweener.xml b/doc/classes/PropertyTweener.xml index d3875ddfc2..76cf4cbfeb 100644 --- a/doc/classes/PropertyTweener.xml +++ b/doc/classes/PropertyTweener.xml @@ -5,6 +5,7 @@ </brief_description> <description> [PropertyTweener] is used to interpolate a property in an object. See [method Tween.tween_property] for more usage information. + The tweener will finish automatically if the target object is freed. [b]Note:[/b] [method Tween.tween_property] is the only correct way to create [PropertyTweener]. Any [PropertyTweener] created manually will not function correctly. </description> <tutorials> @@ -14,10 +15,10 @@ <return type="PropertyTweener" /> <description> When called, the final value will be used as a relative value instead. - [b]Example:[/b] + [b]Example:[/b] Move the node by [code]100[/code] pixels to the right. [codeblock] var tween = get_tree().create_tween() - tween.tween_property(self, "position", Vector2.RIGHT * 100, 1).as_relative() #the node will move by 100 pixels to the right + tween.tween_property(self, "position", Vector2.RIGHT * 100, 1).as_relative() [/codeblock] </description> </method> @@ -26,10 +27,10 @@ <param index="0" name="value" type="Variant" /> <description> Sets a custom initial value to the [PropertyTweener]. - [b]Example:[/b] + [b]Example:[/b] Move the node from position [code](100, 100)[/code] to [code](200, 100)[/code]. [codeblock] var tween = get_tree().create_tween() - tween.tween_property(self, "position", Vector2(200, 100), 1).from(Vector2(100, 100)) #this will move the node from position (100, 100) to (200, 100) + tween.tween_property(self, "position", Vector2(200, 100), 1).from(Vector2(100, 100)) [/codeblock] </description> </method> @@ -48,7 +49,6 @@ <param index="0" name="interpolator_method" type="Callable" /> <description> Allows interpolating the value with a custom easing function. The provided [param interpolator_method] will be called with a value ranging from [code]0.0[/code] to [code]1.0[/code] and is expected to return a value within the same range (values outside the range can be used for overshoot). The return value of the method is then used for interpolation between initial and final value. Note that the parameter passed to the method is still subject to the tweener's own easing. - [b]Example:[/b] [codeblock] @export var curve: Curve diff --git a/doc/classes/RenderingServer.xml b/doc/classes/RenderingServer.xml index c81d5d4fab..4cdfba17e9 100644 --- a/doc/classes/RenderingServer.xml +++ b/doc/classes/RenderingServer.xml @@ -412,6 +412,14 @@ [b]Note:[/b] [param count] is unused and can be left unspecified. </description> </method> + <method name="canvas_item_attach_skeleton"> + <return type="void" /> + <param index="0" name="item" type="RID" /> + <param index="1" name="skeleton" type="RID" /> + <description> + Attaches a skeleton to the [CanvasItem]. Removes the previous skeleton. + </description> + </method> <method name="canvas_item_clear"> <return type="void" /> <param index="0" name="item" type="RID" /> diff --git a/doc/classes/Resource.xml b/doc/classes/Resource.xml index 74d083594f..fe09472c14 100644 --- a/doc/classes/Resource.xml +++ b/doc/classes/Resource.xml @@ -14,7 +14,7 @@ <link title="When and how to avoid using nodes for everything">$DOCS_URL/tutorials/best_practices/node_alternatives.html</link> </tutorials> <methods> - <method name="_get_rid" qualifiers="virtual"> + <method name="_get_rid" qualifiers="virtual const"> <return type="RID" /> <description> Override this method to return a custom [RID] when [method get_rid] is called. diff --git a/doc/classes/ResourceImporterWAV.xml b/doc/classes/ResourceImporterWAV.xml index 8f118ace03..3caa66d262 100644 --- a/doc/classes/ResourceImporterWAV.xml +++ b/doc/classes/ResourceImporterWAV.xml @@ -4,17 +4,18 @@ Imports a WAV audio file for playback. </brief_description> <description> - WAV is an uncompressed format, which can provide higher quality compared to Ogg Vorbis and MP3. It also has the lowest CPU cost to decode. This means high numbers of WAV sounds can be played at the same time, even on low-end deviceS. + WAV is an uncompressed format, which can provide higher quality compared to Ogg Vorbis and MP3. It also has the lowest CPU cost to decode. This means high numbers of WAV sounds can be played at the same time, even on low-end devices. + By default, Godot imports WAV files using the lossy Quite OK Audio compression. You may change this by setting the [member compress/mode] property. </description> <tutorials> <link title="Importing audio samples">$DOCS_URL/tutorials/assets_pipeline/importing_audio_samples.html</link> </tutorials> <members> - <member name="compress/mode" type="int" setter="" getter="" default="0"> + <member name="compress/mode" type="int" setter="" getter="" default="2"> The compression mode to use on import. - [b]Disabled:[/b] Imports audio data without any compression. This results in the highest possible quality. - [b]RAM (Ima-ADPCM):[/b] Performs fast lossy compression on import. Low CPU cost, but quality is noticeably decreased compared to Ogg Vorbis or even MP3. - [b]QOA ([url=https://qoaformat.org/]Quite OK Audio[/url]):[/b] Performs lossy compression on import. CPU cost is slightly higher than IMA-ADPCM, but quality is much higher. + - [b]PCM (Uncompressed):[/b] Imports audio data without any form of compression, preserving the highest possible quality. It has the lowest CPU cost, but the highest memory usage. + - [b]IMA ADPCM:[/b] Applies fast, lossy compression during import, noticeably decreasing the quality, but with low CPU cost and memory usage. Does not support seeking and only Forward loop mode is supported. + - [b][url=https://qoaformat.org/]Quite OK Audio[/url]:[/b] Also applies lossy compression on import, having a slightly higher CPU cost compared to IMA ADPCM, but much higher quality and the lowest memory usage. </member> <member name="edit/loop_begin" type="int" setter="" getter="" default="0"> The begin loop point to use when [member edit/loop_mode] is [b]Forward[/b], [b]Ping-Pong[/b], or [b]Backward[/b]. This is set in samples after the beginning of the audio file. @@ -23,11 +24,12 @@ The end loop point to use when [member edit/loop_mode] is [b]Forward[/b], [b]Ping-Pong[/b], or [b]Backward[/b]. This is set in samples after the beginning of the audio file. A value of [code]-1[/code] uses the end of the audio file as the end loop point. </member> <member name="edit/loop_mode" type="int" setter="" getter="" default="0"> - Controls how audio should loop. This is automatically read from the WAV metadata on import. - [b]Disabled:[/b] Don't loop audio, even if metadata indicates the file should be played back looping. - [b]Forward:[/b] Standard audio looping. - [b]Ping-Pong:[/b] Play audio forward until it's done playing, then play it backward and repeat. This is similar to mirrored texture repeat, but for audio. - [b]Backward:[/b] Play audio in reverse and loop back to the end when done playing. + Controls how audio should loop. + - [b]Detect From WAV:[/b] Uses loop information from the WAV metadata. + - [b]Disabled:[/b] Don't loop audio, even if the metadata indicates the file playback should loop. + - [b]Forward:[/b] Standard audio looping. Plays the audio forward from the beginning to [member edit/loop_end], then returns to [member edit/loop_begin] and repeats. + - [b]Ping-Pong:[/b] Plays the audio forward until [member edit/loop_end], then backwards to [member edit/loop_begin], repeating this cycle. + - [b]Backward:[/b] Plays the audio backwards from [member edit/loop_end] to [member edit/loop_begin], then repeats. [b]Note:[/b] In [AudioStreamPlayer], the [signal AudioStreamPlayer.finished] signal won't be emitted for looping audio when it reaches the end of the audio file, as the audio will keep playing indefinitely. </member> <member name="edit/normalize" type="bool" setter="" getter="" default="false"> diff --git a/doc/classes/ResourceLoader.xml b/doc/classes/ResourceLoader.xml index cb0db46595..56c3208fc3 100644 --- a/doc/classes/ResourceLoader.xml +++ b/doc/classes/ResourceLoader.xml @@ -31,6 +31,14 @@ [b]Note:[/b] If you use [method Resource.take_over_path], this method will return [code]true[/code] for the taken path even if the resource wasn't saved (i.e. exists only in resource cache). </description> </method> + <method name="get_cached_ref"> + <return type="Resource" /> + <param index="0" name="path" type="String" /> + <description> + Returns the cached resource reference for the given [param path]. + [b]Note:[/b] If the resource is not cached, the returned [Resource] will be invalid. + </description> + </method> <method name="get_dependencies"> <return type="PackedStringArray" /> <param index="0" name="path" type="String" /> diff --git a/doc/classes/ResourceUID.xml b/doc/classes/ResourceUID.xml index 8b990b132a..24853d462d 100644 --- a/doc/classes/ResourceUID.xml +++ b/doc/classes/ResourceUID.xml @@ -4,7 +4,7 @@ A singleton that manages the unique identifiers of all resources within a project. </brief_description> <description> - Resource UIDs (Unique IDentifiers) allow the engine to keep references between resources intact, even if files can renamed or moved. They can be accessed with [code]uid://[/code]. + Resource UIDs (Unique IDentifiers) allow the engine to keep references between resources intact, even if files are renamed or moved. They can be accessed with [code]uid://[/code]. [ResourceUID] keeps track of all registered resource UIDs in a project, generates new UIDs, and converts between their string and integer representations. </description> <tutorials> diff --git a/doc/classes/RichTextLabel.xml b/doc/classes/RichTextLabel.xml index 7e0c39ac7c..4a2cbbc3d8 100644 --- a/doc/classes/RichTextLabel.xml +++ b/doc/classes/RichTextLabel.xml @@ -70,7 +70,7 @@ <param index="0" name="character" type="int" /> <description> Returns the line number of the character position provided. Line and character numbers are both zero-indexed. - [b]Note:[/b] If [member threaded] is enabled, this method returns a value for the loaded part of the document. Use [method is_ready] or [signal finished] to determine whether document is fully loaded. + [b]Note:[/b] If [member threaded] is enabled, this method returns a value for the loaded part of the document. Use [method is_finished] or [signal finished] to determine whether document is fully loaded. </description> </method> <method name="get_character_paragraph"> @@ -78,28 +78,28 @@ <param index="0" name="character" type="int" /> <description> Returns the paragraph number of the character position provided. Paragraph and character numbers are both zero-indexed. - [b]Note:[/b] If [member threaded] is enabled, this method returns a value for the loaded part of the document. Use [method is_ready] or [signal finished] to determine whether document is fully loaded. + [b]Note:[/b] If [member threaded] is enabled, this method returns a value for the loaded part of the document. Use [method is_finished] or [signal finished] to determine whether document is fully loaded. </description> </method> <method name="get_content_height" qualifiers="const"> <return type="int" /> <description> Returns the height of the content. - [b]Note:[/b] If [member threaded] is enabled, this method returns a value for the loaded part of the document. Use [method is_ready] or [signal finished] to determine whether document is fully loaded. + [b]Note:[/b] If [member threaded] is enabled, this method returns a value for the loaded part of the document. Use [method is_finished] or [signal finished] to determine whether document is fully loaded. </description> </method> <method name="get_content_width" qualifiers="const"> <return type="int" /> <description> Returns the width of the content. - [b]Note:[/b] If [member threaded] is enabled, this method returns a value for the loaded part of the document. Use [method is_ready] or [signal finished] to determine whether document is fully loaded. + [b]Note:[/b] If [member threaded] is enabled, this method returns a value for the loaded part of the document. Use [method is_finished] or [signal finished] to determine whether document is fully loaded. </description> </method> <method name="get_line_count" qualifiers="const"> <return type="int" /> <description> Returns the total number of lines in the text. Wrapped text is counted as multiple lines. - [b]Note:[/b] If [member threaded] is enabled, this method returns a value for the loaded part of the document. Use [method is_ready] or [signal finished] to determine whether document is fully loaded. + [b]Note:[/b] If [member threaded] is enabled, this method returns a value for the loaded part of the document. Use [method is_finished] or [signal finished] to determine whether document is fully loaded. </description> </method> <method name="get_line_offset"> @@ -107,7 +107,7 @@ <param index="0" name="line" type="int" /> <description> Returns the vertical offset of the line found at the provided index. - [b]Note:[/b] If [member threaded] is enabled, this method returns a value for the loaded part of the document. Use [method is_ready] or [signal finished] to determine whether document is fully loaded. + [b]Note:[/b] If [member threaded] is enabled, this method returns a value for the loaded part of the document. Use [method is_finished] or [signal finished] to determine whether document is fully loaded. </description> </method> <method name="get_menu" qualifiers="const"> @@ -167,7 +167,7 @@ <param index="0" name="paragraph" type="int" /> <description> Returns the vertical offset of the paragraph found at the provided index. - [b]Note:[/b] If [member threaded] is enabled, this method returns a value for the loaded part of the document. Use [method is_ready] or [signal finished] to determine whether document is fully loaded. + [b]Note:[/b] If [member threaded] is enabled, this method returns a value for the loaded part of the document. Use [method is_finished] or [signal finished] to determine whether document is fully loaded. </description> </method> <method name="get_parsed_text" qualifiers="const"> @@ -211,14 +211,14 @@ <return type="int" /> <description> Returns the number of visible lines. - [b]Note:[/b] If [member threaded] is enabled, this method returns a value for the loaded part of the document. Use [method is_ready] or [signal finished] to determine whether document is fully loaded. + [b]Note:[/b] If [member threaded] is enabled, this method returns a value for the loaded part of the document. Use [method is_finished] or [signal finished] to determine whether document is fully loaded. </description> </method> <method name="get_visible_paragraph_count" qualifiers="const"> <return type="int" /> <description> Returns the number of visible paragraphs. A paragraph is considered visible if at least one of its lines is visible. - [b]Note:[/b] If [member threaded] is enabled, this method returns a value for the loaded part of the document. Use [method is_ready] or [signal finished] to determine whether document is fully loaded. + [b]Note:[/b] If [member threaded] is enabled, this method returns a value for the loaded part of the document. Use [method is_finished] or [signal finished] to determine whether document is fully loaded. </description> </method> <method name="install_effect"> @@ -256,13 +256,19 @@ Invalidates [param paragraph] and all subsequent paragraphs cache. </description> </method> + <method name="is_finished" qualifiers="const"> + <return type="bool" /> + <description> + If [member threaded] is enabled, returns [code]true[/code] if the background thread has finished text processing, otherwise always return [code]true[/code]. + </description> + </method> <method name="is_menu_visible" qualifiers="const"> <return type="bool" /> <description> Returns whether the menu is visible. Use this instead of [code]get_menu().visible[/code] to improve performance (so the creation of the menu is avoided). </description> </method> - <method name="is_ready" qualifiers="const"> + <method name="is_ready" qualifiers="const" deprecated="Use [method is_finished] instead."> <return type="bool" /> <description> If [member threaded] is enabled, returns [code]true[/code] if the background thread has finished text processing, otherwise always return [code]true[/code]. @@ -625,6 +631,12 @@ <member name="hint_underlined" type="bool" setter="set_hint_underline" getter="is_hint_underlined" default="true"> If [code]true[/code], the label underlines hint tags such as [code skip-lint][hint=description]{text}[/hint][/code]. </member> + <member name="horizontal_alignment" type="int" setter="set_horizontal_alignment" getter="get_horizontal_alignment" enum="HorizontalAlignment" default="0"> + Controls the text's horizontal alignment. Supports left, center, right, and fill, or justify. Set it to one of the [enum HorizontalAlignment] constants. + </member> + <member name="justification_flags" type="int" setter="set_justification_flags" getter="get_justification_flags" enum="TextServer.JustificationFlag" is_bitfield="true" default="163"> + Line fill alignment rules. See [enum TextServer.JustificationFlag] for more information. + </member> <member name="language" type="String" setter="set_language" getter="get_language" default=""""> Language code used for line-breaking and text shaping algorithms, if left empty current locale is used instead. </member> @@ -656,6 +668,9 @@ <member name="tab_size" type="int" setter="set_tab_size" getter="get_tab_size" default="4"> The number of spaces associated with a single tab length. Does not affect [code]\t[/code] in text tags, only indent tags. </member> + <member name="tab_stops" type="PackedFloat32Array" setter="set_tab_stops" getter="get_tab_stops" default="PackedFloat32Array()"> + Aligns text to the given tab-stops. + </member> <member name="text" type="String" setter="set_text" getter="get_text" default=""""> The label's text in BBCode format. Is not representative of manual modifications to the internal tag stack. Erases changes made by other methods when edited. [b]Note:[/b] If [member bbcode_enabled] is [code]true[/code], it is unadvised to use the [code]+=[/code] operator with [member text] (e.g. [code]text += "some string"[/code]) as it replaces the whole text and can cause slowdowns. It will also erase all BBCode that was added to stack using [code]push_*[/code] methods. Use [method append_text] for adding text instead, unless you absolutely need to close a tag that was opened in an earlier method call. diff --git a/doc/classes/ScriptEditor.xml b/doc/classes/ScriptEditor.xml index 43ee4dda60..5cf077c266 100644 --- a/doc/classes/ScriptEditor.xml +++ b/doc/classes/ScriptEditor.xml @@ -10,6 +10,12 @@ <tutorials> </tutorials> <methods> + <method name="get_breakpoints"> + <return type="PackedStringArray" /> + <description> + Returns array of breakpoints. + </description> + </method> <method name="get_current_editor" qualifiers="const"> <return type="ScriptEditorBase" /> <description> @@ -40,7 +46,6 @@ <description> Opens help for the given topic. The [param topic] is an encoded string that controls which class, method, constant, signal, annotation, property, or theme item should be focused. The supported [param topic] formats include [code]class_name:class[/code], [code]class_method:class:method[/code], [code]class_constant:class:constant[/code], [code]class_signal:class:signal[/code], [code]class_annotation:class:@annotation[/code], [code]class_property:class:property[/code], and [code]class_theme_item:class:item[/code], where [code]class[/code] is the class name, [code]method[/code] is the method name, [code]constant[/code] is the constant name, [code]signal[/code] is the signal name, [code]annotation[/code] is the annotation name, [code]property[/code] is the property name, and [code]item[/code] is the theme item. - [b]Examples:[/b] [codeblock] # Shows help for the Node class. class_name:Node diff --git a/doc/classes/Semaphore.xml b/doc/classes/Semaphore.xml index d0db24dfb7..3ecb5c23af 100644 --- a/doc/classes/Semaphore.xml +++ b/doc/classes/Semaphore.xml @@ -17,8 +17,9 @@ <methods> <method name="post"> <return type="void" /> + <param index="0" name="count" type="int" default="1" /> <description> - Lowers the [Semaphore], allowing one more thread in. + Lowers the [Semaphore], allowing one thread in, or more if [param count] is specified. </description> </method> <method name="try_wait"> diff --git a/doc/classes/Shader.xml b/doc/classes/Shader.xml index b71f9ca1b0..68176dea14 100644 --- a/doc/classes/Shader.xml +++ b/doc/classes/Shader.xml @@ -12,7 +12,7 @@ </tutorials> <methods> <method name="get_default_texture_parameter" qualifiers="const"> - <return type="Texture2D" /> + <return type="Texture" /> <param index="0" name="name" type="StringName" /> <param index="1" name="index" type="int" default="0" /> <description> @@ -38,7 +38,7 @@ <method name="set_default_texture_parameter"> <return type="void" /> <param index="0" name="name" type="StringName" /> - <param index="1" name="texture" type="Texture2D" /> + <param index="1" name="texture" type="Texture" /> <param index="2" name="index" type="int" default="0" /> <description> Sets the default texture to be used with a texture uniform. The default is used if a texture is not set in the [ShaderMaterial]. diff --git a/doc/classes/Signal.xml b/doc/classes/Signal.xml index 07e15d0b23..7d6ff1e9b0 100644 --- a/doc/classes/Signal.xml +++ b/doc/classes/Signal.xml @@ -119,7 +119,7 @@ <method name="is_null" qualifiers="const"> <return type="bool" /> <description> - Returns [code]true[/code] if the signal's name does not exist in its object, or the object is not valid. + Returns [code]true[/code] if this [Signal] has no object and the signal name is empty. Equivalent to [code]signal == Signal()[/code]. </description> </method> </methods> diff --git a/doc/classes/SpinBox.xml b/doc/classes/SpinBox.xml index 03e247ec8a..3fb30a81b8 100644 --- a/doc/classes/SpinBox.xml +++ b/doc/classes/SpinBox.xml @@ -5,7 +5,7 @@ </brief_description> <description> [SpinBox] is a numerical input text field. It allows entering integers and floating-point numbers. - [b]Example:[/b] + [b]Example:[/b] Create a [SpinBox], disable its context menu and set its text alignment to right. [codeblocks] [gdscript] var spin_box = SpinBox.new() @@ -22,10 +22,9 @@ spinBox.AlignHorizontal = LineEdit.HorizontalAlignEnum.Right; [/csharp] [/codeblocks] - The above code will create a [SpinBox], disable context menu on it and set the text alignment to right. See [Range] class for more options over the [SpinBox]. [b]Note:[/b] With the [SpinBox]'s context menu disabled, you can right-click the bottom half of the spinbox to set the value to its minimum, while right-clicking the top half sets the value to its maximum. - [b]Note:[/b] [SpinBox] relies on an underlying [LineEdit] node. To theme a [SpinBox]'s background, add theme items for [LineEdit] and customize them. + [b]Note:[/b] [SpinBox] relies on an underlying [LineEdit] node. To theme a [SpinBox]'s background, add theme items for [LineEdit] and customize them. The [LineEdit] has the [code]SpinBoxInnerLineEdit[/code] theme variation, so that you can give it a distinct appearance from regular [LineEdit]s. [b]Note:[/b] If you want to implement drag and drop for the underlying [LineEdit], you can use [method Control.set_drag_forwarding] on the node returned by [method get_line_edit]. </description> <tutorials> @@ -71,8 +70,98 @@ </member> </members> <theme_items> + <theme_item name="down_disabled_icon_modulate" data_type="color" type="Color" default="Color(0.875, 0.875, 0.875, 0.5)"> + Down button icon modulation color, when the button is disabled. + </theme_item> + <theme_item name="down_hover_icon_modulate" data_type="color" type="Color" default="Color(0.95, 0.95, 0.95, 1)"> + Down button icon modulation color, when the button is hovered. + </theme_item> + <theme_item name="down_icon_modulate" data_type="color" type="Color" default="Color(0.875, 0.875, 0.875, 1)"> + Down button icon modulation color. + </theme_item> + <theme_item name="down_pressed_icon_modulate" data_type="color" type="Color" default="Color(0.95, 0.95, 0.95, 1)"> + Down button icon modulation color, when the button is being pressed. + </theme_item> + <theme_item name="up_disabled_icon_modulate" data_type="color" type="Color" default="Color(0.875, 0.875, 0.875, 0.5)"> + Up button icon modulation color, when the button is disabled. + </theme_item> + <theme_item name="up_hover_icon_modulate" data_type="color" type="Color" default="Color(0.95, 0.95, 0.95, 1)"> + Up button icon modulation color, when the button is hovered. + </theme_item> + <theme_item name="up_icon_modulate" data_type="color" type="Color" default="Color(0.875, 0.875, 0.875, 1)"> + Up button icon modulation color. + </theme_item> + <theme_item name="up_pressed_icon_modulate" data_type="color" type="Color" default="Color(0.95, 0.95, 0.95, 1)"> + Up button icon modulation color, when the button is being pressed. + </theme_item> + <theme_item name="buttons_vertical_separation" data_type="constant" type="int" default="0"> + Vertical separation between the up and down buttons. + </theme_item> + <theme_item name="buttons_width" data_type="constant" type="int" default="16"> + Width of the up and down buttons. If smaller than any icon set on the buttons, the respective icon may overlap neighboring elements, unless [theme_item set_min_buttons_width_from_icons] is different than [code]0[/code]. + </theme_item> + <theme_item name="field_and_buttons_separation" data_type="constant" type="int" default="2"> + Width of the horizontal separation between the text input field ([LineEdit]) and the buttons. + </theme_item> + <theme_item name="set_min_buttons_width_from_icons" data_type="constant" type="int" default="1"> + If not [code]0[/code], the minimum button width corresponds to the widest of all icons set on those buttons, even if [theme_item buttons_width] is smaller. + </theme_item> + <theme_item name="down" data_type="icon" type="Texture2D"> + Down button icon, displayed in the middle of the down (value-decreasing) button. + </theme_item> + <theme_item name="down_disabled" data_type="icon" type="Texture2D"> + Down button icon when the button is disabled. + </theme_item> + <theme_item name="down_hover" data_type="icon" type="Texture2D"> + Down button icon when the button is hovered. + </theme_item> + <theme_item name="down_pressed" data_type="icon" type="Texture2D"> + Down button icon when the button is being pressed. + </theme_item> + <theme_item name="up" data_type="icon" type="Texture2D"> + Up button icon, displayed in the middle of the up (value-increasing) button. + </theme_item> + <theme_item name="up_disabled" data_type="icon" type="Texture2D"> + Up button icon when the button is disabled. + </theme_item> + <theme_item name="up_hover" data_type="icon" type="Texture2D"> + Up button icon when the button is hovered. + </theme_item> + <theme_item name="up_pressed" data_type="icon" type="Texture2D"> + Up button icon when the button is being pressed. + </theme_item> <theme_item name="updown" data_type="icon" type="Texture2D"> - Sets a custom [Texture2D] for up and down arrows of the [SpinBox]. + Single texture representing both the up and down buttons icons. It is displayed in the middle of the buttons and does not change upon interaction. It is recommended to use individual [theme_item up] and [theme_item down] graphics for better usability. This can also be used as additional decoration between the two buttons. + </theme_item> + <theme_item name="down_background" data_type="style" type="StyleBox"> + Background style of the down button. + </theme_item> + <theme_item name="down_background_disabled" data_type="style" type="StyleBox"> + Background style of the down button when disabled. + </theme_item> + <theme_item name="down_background_hovered" data_type="style" type="StyleBox"> + Background style of the down button when hovered. + </theme_item> + <theme_item name="down_background_pressed" data_type="style" type="StyleBox"> + Background style of the down button when being pressed. + </theme_item> + <theme_item name="field_and_buttons_separator" data_type="style" type="StyleBox"> + [StyleBox] drawn in the space occupied by the separation between the input field and the buttons. + </theme_item> + <theme_item name="up_background" data_type="style" type="StyleBox"> + Background style of the up button. + </theme_item> + <theme_item name="up_background_disabled" data_type="style" type="StyleBox"> + Background style of the up button when disabled. + </theme_item> + <theme_item name="up_background_hovered" data_type="style" type="StyleBox"> + Background style of the up button when hovered. + </theme_item> + <theme_item name="up_background_pressed" data_type="style" type="StyleBox"> + Background style of the up button when being pressed. + </theme_item> + <theme_item name="up_down_buttons_separator" data_type="style" type="StyleBox"> + [StyleBox] drawn in the space occupied by the separation between the up and down buttons. </theme_item> </theme_items> </class> diff --git a/doc/classes/Sprite2D.xml b/doc/classes/Sprite2D.xml index 10ac4b0fcc..d73cb02d94 100644 --- a/doc/classes/Sprite2D.xml +++ b/doc/classes/Sprite2D.xml @@ -13,8 +13,8 @@ <method name="get_rect" qualifiers="const"> <return type="Rect2" /> <description> - Returns a [Rect2] representing the Sprite2D's boundary in local coordinates. Can be used to detect if the Sprite2D was clicked. - [b]Example:[/b] + Returns a [Rect2] representing the Sprite2D's boundary in local coordinates. + [b]Example:[/b] Detect if the Sprite2D was clicked: [codeblocks] [gdscript] func _input(event): diff --git a/doc/classes/SpriteFrames.xml b/doc/classes/SpriteFrames.xml index fd8c15c560..b891f4adcd 100644 --- a/doc/classes/SpriteFrames.xml +++ b/doc/classes/SpriteFrames.xml @@ -39,6 +39,14 @@ Removes all animations. An empty [code]default[/code] animation will be created. </description> </method> + <method name="duplicate_animation"> + <return type="void" /> + <param index="0" name="anim_from" type="StringName" /> + <param index="1" name="anim_to" type="StringName" /> + <description> + Duplicates the animation [param anim_from] to a new animation named [param anim_to]. Fails if [param anim_to] already exists, or if [param anim_from] does not exist. + </description> + </method> <method name="get_animation_loop" qualifiers="const"> <return type="bool" /> <param index="0" name="anim" type="StringName" /> diff --git a/doc/classes/String.xml b/doc/classes/String.xml index 450e483f69..d99eaa64a6 100644 --- a/doc/classes/String.xml +++ b/doc/classes/String.xml @@ -324,7 +324,6 @@ <description> Splits the string using a [param delimiter] and returns the substring at index [param slice]. Returns the original string if [param delimiter] does not occur in the string. Returns an empty string if the [param slice] does not exist. This is faster than [method split], if you only need one substring. - [b]Example:[/b] [codeblock] print("i/am/example/hi".get_slice("/", 2)) # Prints "example" [/codeblock] @@ -452,6 +451,19 @@ Returns [code]true[/code] if all characters of this string can be found in [param text] in their original order, [b]ignoring case[/b]. </description> </method> + <method name="is_valid_ascii_identifier" qualifiers="const"> + <return type="bool" /> + <description> + Returns [code]true[/code] if this string is a valid ASCII identifier. A valid ASCII identifier may contain only letters, digits, and underscores ([code]_[/code]), and the first character may not be a digit. + [codeblock] + print("node_2d".is_valid_ascii_identifier()) # Prints true + print("TYPE_FLOAT".is_valid_ascii_identifier()) # Prints true + print("1st_method".is_valid_ascii_identifier()) # Prints false + print("MyMethod#2".is_valid_ascii_identifier()) # Prints false + [/codeblock] + See also [method is_valid_unicode_identifier]. + </description> + </method> <method name="is_valid_filename" qualifiers="const"> <return type="bool" /> <description> @@ -491,7 +503,7 @@ Returns [code]true[/code] if this string is a valid color in hexadecimal HTML notation. The string must be a hexadecimal value (see [method is_valid_hex_number]) of either 3, 4, 6 or 8 digits, and may be prefixed by a hash sign ([code]#[/code]). Other HTML notations for colors, such as names or [code]hsl()[/code], are not considered valid. See also [method Color.html]. </description> </method> - <method name="is_valid_identifier" qualifiers="const"> + <method name="is_valid_identifier" qualifiers="const" deprecated="Use [method is_valid_ascii_identifier] instead."> <return type="bool" /> <description> Returns [code]true[/code] if this string is a valid identifier. A valid identifier may contain only letters, digits and underscores ([code]_[/code]), and the first character may not be a digit. @@ -522,12 +534,28 @@ Returns [code]true[/code] if this string represents a well-formatted IPv4 or IPv6 address. This method considers [url=https://en.wikipedia.org/wiki/Reserved_IP_addresses]reserved IP addresses[/url] such as [code]"0.0.0.0"[/code] and [code]"ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"[/code] as valid. </description> </method> + <method name="is_valid_unicode_identifier" qualifiers="const"> + <return type="bool" /> + <description> + Returns [code]true[/code] if this string is a valid Unicode identifier. + A valid Unicode identifier must begin with a Unicode character of class [code]XID_Start[/code] or [code]"_"[/code], and may contain Unicode characters of class [code]XID_Continue[/code] in the other positions. + [codeblock] + print("node_2d".is_valid_unicode_identifier()) # Prints true + print("1st_method".is_valid_unicode_identifier()) # Prints false + print("MyMethod#2".is_valid_unicode_identifier()) # Prints false + print("állóképesség".is_valid_unicode_identifier()) # Prints true + print("выносливость".is_valid_unicode_identifier()) # Prints true + print("体力".is_valid_unicode_identifier()) # Prints true + [/codeblock] + See also [method is_valid_ascii_identifier]. + [b]Note:[/b] This method checks identifiers the same way as GDScript. See [method TextServer.is_valid_identifier] for more advanced checks. + </description> + </method> <method name="join" qualifiers="const"> <return type="String" /> <param index="0" name="parts" type="PackedStringArray" /> <description> Returns the concatenation of [param parts]' elements, with each element separated by the string calling this method. This method is the opposite of [method split]. - [b]Example:[/b] [codeblocks] [gdscript] var fruits = ["Apple", "Orange", "Pear", "Kiwi"] @@ -647,7 +675,6 @@ Converts a [float] to a string representation of a decimal number, with the number of decimal places specified in [param decimals]. If [param decimals] is [code]-1[/code] as by default, the string representation may only have up to 14 significant digits, with digits before the decimal point having priority over digits after. Trailing zeros are not included in the string. The last digit is rounded, not truncated. - [b]Example:[/b] [codeblock] String.num(3.141593) # Returns "3.141593" String.num(3.141593, 3) # Returns "3.142" @@ -802,7 +829,6 @@ Splits the string using a [param delimiter] and returns an array of the substrings, starting from the end of the string. The splits in the returned array appear in the same order as the original string. If [param delimiter] is an empty string, each substring will be a single character. If [param allow_empty] is [code]false[/code], empty strings between adjacent delimiters are excluded from the array. If [param maxsplit] is greater than [code]0[/code], the number of splits may not exceed [param maxsplit]. By default, the entire string is split, which is mostly identical to [method split]. - [b]Example:[/b] [codeblocks] [gdscript] var some_string = "One,Two,Three,Four" @@ -882,7 +908,6 @@ Splits the string using a [param delimiter] and returns an array of the substrings. If [param delimiter] is an empty string, each substring will be a single character. This method is the opposite of [method join]. If [param allow_empty] is [code]false[/code], empty strings between adjacent delimiters are excluded from the array. If [param maxsplit] is greater than [code]0[/code], the number of splits may not exceed [param maxsplit]. By default, the entire string is split. - [b]Example:[/b] [codeblocks] [gdscript] var some_array = "One,Two,Three,Four".split(",", true, 2) diff --git a/doc/classes/StringName.xml b/doc/classes/StringName.xml index 76586b7968..1a891de05f 100644 --- a/doc/classes/StringName.xml +++ b/doc/classes/StringName.xml @@ -301,7 +301,6 @@ <description> Splits the string using a [param delimiter] and returns the substring at index [param slice]. Returns an empty string if the [param slice] does not exist. This is faster than [method split], if you only need one substring. - [b]Example:[/b] [codeblock] print("i/am/example/hi".get_slice("/", 2)) # Prints "example" [/codeblock] @@ -421,6 +420,19 @@ Returns [code]true[/code] if all characters of this string can be found in [param text] in their original order, [b]ignoring case[/b]. </description> </method> + <method name="is_valid_ascii_identifier" qualifiers="const"> + <return type="bool" /> + <description> + Returns [code]true[/code] if this string is a valid ASCII identifier. A valid ASCII identifier may contain only letters, digits, and underscores ([code]_[/code]), and the first character may not be a digit. + [codeblock] + print("node_2d".is_valid_ascii_identifier()) # Prints true + print("TYPE_FLOAT".is_valid_ascii_identifier()) # Prints true + print("1st_method".is_valid_ascii_identifier()) # Prints false + print("MyMethod#2".is_valid_ascii_identifier()) # Prints false + [/codeblock] + See also [method is_valid_unicode_identifier]. + </description> + </method> <method name="is_valid_filename" qualifiers="const"> <return type="bool" /> <description> @@ -460,7 +472,7 @@ Returns [code]true[/code] if this string is a valid color in hexadecimal HTML notation. The string must be a hexadecimal value (see [method is_valid_hex_number]) of either 3, 4, 6 or 8 digits, and may be prefixed by a hash sign ([code]#[/code]). Other HTML notations for colors, such as names or [code]hsl()[/code], are not considered valid. See also [method Color.html]. </description> </method> - <method name="is_valid_identifier" qualifiers="const"> + <method name="is_valid_identifier" qualifiers="const" deprecated="Use [method is_valid_ascii_identifier] instead."> <return type="bool" /> <description> Returns [code]true[/code] if this string is a valid identifier. A valid identifier may contain only letters, digits and underscores ([code]_[/code]), and the first character may not be a digit. @@ -491,12 +503,28 @@ Returns [code]true[/code] if this string represents a well-formatted IPv4 or IPv6 address. This method considers [url=https://en.wikipedia.org/wiki/Reserved_IP_addresses]reserved IP addresses[/url] such as [code]"0.0.0.0"[/code] and [code]"ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"[/code] as valid. </description> </method> + <method name="is_valid_unicode_identifier" qualifiers="const"> + <return type="bool" /> + <description> + Returns [code]true[/code] if this string is a valid Unicode identifier. + A valid Unicode identifier must begin with a Unicode character of class [code]XID_Start[/code] or [code]"_"[/code], and may contain Unicode characters of class [code]XID_Continue[/code] in the other positions. + [codeblock] + print("node_2d".is_valid_unicode_identifier()) # Prints true + print("1st_method".is_valid_unicode_identifier()) # Prints false + print("MyMethod#2".is_valid_unicode_identifier()) # Prints false + print("állóképesség".is_valid_unicode_identifier()) # Prints true + print("выносливость".is_valid_unicode_identifier()) # Prints true + print("体力".is_valid_unicode_identifier()) # Prints true + [/codeblock] + See also [method is_valid_ascii_identifier]. + [b]Note:[/b] This method checks identifiers the same way as GDScript. See [method TextServer.is_valid_identifier] for more advanced checks. + </description> + </method> <method name="join" qualifiers="const"> <return type="String" /> <param index="0" name="parts" type="PackedStringArray" /> <description> Returns the concatenation of [param parts]' elements, with each element separated by the string calling this method. This method is the opposite of [method split]. - [b]Example:[/b] [codeblocks] [gdscript] var fruits = ["Apple", "Orange", "Pear", "Kiwi"] @@ -703,7 +731,6 @@ Splits the string using a [param delimiter] and returns an array of the substrings, starting from the end of the string. The splits in the returned array appear in the same order as the original string. If [param delimiter] is an empty string, each substring will be a single character. If [param allow_empty] is [code]false[/code], empty strings between adjacent delimiters are excluded from the array. If [param maxsplit] is greater than [code]0[/code], the number of splits may not exceed [param maxsplit]. By default, the entire string is split, which is mostly identical to [method split]. - [b]Example:[/b] [codeblocks] [gdscript] var some_string = "One,Two,Three,Four" @@ -783,7 +810,6 @@ Splits the string using a [param delimiter] and returns an array of the substrings. If [param delimiter] is an empty string, each substring will be a single character. This method is the opposite of [method join]. If [param allow_empty] is [code]false[/code], empty strings between adjacent delimiters are excluded from the array. If [param maxsplit] is greater than [code]0[/code], the number of splits may not exceed [param maxsplit]. By default, the entire string is split. - [b]Example:[/b] [codeblocks] [gdscript] var some_array = "One,Two,Three,Four".split(",", true, 2) diff --git a/doc/classes/StyleBoxFlat.xml b/doc/classes/StyleBoxFlat.xml index 181e1ff77a..8c629b12c5 100644 --- a/doc/classes/StyleBoxFlat.xml +++ b/doc/classes/StyleBoxFlat.xml @@ -5,8 +5,7 @@ </brief_description> <description> By configuring various properties of this style box, you can achieve many common looks without the need of a texture. This includes optionally rounded borders, antialiasing, shadows, and skew. - Setting corner radius to high values is allowed. As soon as corners overlap, the stylebox will switch to a relative system. - [b]Example:[/b] + Setting corner radius to high values is allowed. As soon as corners overlap, the stylebox will switch to a relative system: [codeblock lang=text] height = 30 corner_radius_top_left = 50 diff --git a/doc/classes/TextEdit.xml b/doc/classes/TextEdit.xml index f71601f9ae..6505e48fb9 100644 --- a/doc/classes/TextEdit.xml +++ b/doc/classes/TextEdit.xml @@ -1327,7 +1327,10 @@ Text shown when the [TextEdit] is empty. It is [b]not[/b] the [TextEdit]'s default value (see [member text]). </member> <member name="scroll_fit_content_height" type="bool" setter="set_fit_content_height_enabled" getter="is_fit_content_height_enabled" default="false"> - If [code]true[/code], [TextEdit] will disable vertical scroll and fit minimum height to the number of visible lines. + If [code]true[/code], [TextEdit] will disable vertical scroll and fit minimum height to the number of visible lines. When both this property and [member scroll_fit_content_width] are [code]true[/code], no scrollbars will be displayed. + </member> + <member name="scroll_fit_content_width" type="bool" setter="set_fit_content_width_enabled" getter="is_fit_content_width_enabled" default="false"> + If [code]true[/code], [TextEdit] will disable horizontal scroll and fit minimum width to the widest line in the text. When both this property and [member scroll_fit_content_height] are [code]true[/code], no scrollbars will be displayed. </member> <member name="scroll_horizontal" type="int" setter="set_h_scroll" getter="get_h_scroll" default="0"> If there is a horizontal scrollbar, this determines the current horizontal scroll value in pixels. diff --git a/doc/classes/TextMesh.xml b/doc/classes/TextMesh.xml index 9e705311c5..898d19aed3 100644 --- a/doc/classes/TextMesh.xml +++ b/doc/classes/TextMesh.xml @@ -31,7 +31,7 @@ Controls the text's horizontal alignment. Supports left, center, right, and fill, or justify. Set it to one of the [enum HorizontalAlignment] constants. </member> <member name="justification_flags" type="int" setter="set_justification_flags" getter="get_justification_flags" enum="TextServer.JustificationFlag" is_bitfield="true" default="163"> - Line fill alignment rules. For more info see [enum TextServer.JustificationFlag]. + Line fill alignment rules. See [enum TextServer.JustificationFlag] for more information. </member> <member name="language" type="String" setter="set_language" getter="get_language" default=""""> Language code used for text shaping algorithms, if left empty current locale is used instead. diff --git a/doc/classes/TextParagraph.xml b/doc/classes/TextParagraph.xml index c6511a2b8e..46197f19b8 100644 --- a/doc/classes/TextParagraph.xml +++ b/doc/classes/TextParagraph.xml @@ -278,7 +278,7 @@ Ellipsis character used for text clipping. </member> <member name="justification_flags" type="int" setter="set_justification_flags" getter="get_justification_flags" enum="TextServer.JustificationFlag" is_bitfield="true" default="163"> - Line fill alignment rules. For more info see [enum TextServer.JustificationFlag]. + Line fill alignment rules. See [enum TextServer.JustificationFlag] for more information. </member> <member name="max_lines_visible" type="int" setter="set_max_lines_visible" getter="get_max_lines_visible" default="-1"> Limits the lines of text shown. diff --git a/doc/classes/TileData.xml b/doc/classes/TileData.xml index 91df90580c..9468763b6e 100644 --- a/doc/classes/TileData.xml +++ b/doc/classes/TileData.xml @@ -134,7 +134,7 @@ <param index="1" name="polygon_index" type="int" /> <param index="2" name="one_way_margin" type="float" /> <description> - Enables/disables one-way collisions on the polygon at index [param polygon_index] for TileSet physics layer with index [param layer_id]. + Sets the one-way margin (for one-way platforms) of the polygon at index [param polygon_index] for TileSet physics layer with index [param layer_id]. </description> </method> <method name="set_collision_polygon_points"> diff --git a/doc/classes/TileMapLayer.xml b/doc/classes/TileMapLayer.xml index bead1c32c0..135f85de69 100644 --- a/doc/classes/TileMapLayer.xml +++ b/doc/classes/TileMapLayer.xml @@ -284,6 +284,9 @@ <member name="navigation_visibility_mode" type="int" setter="set_navigation_visibility_mode" getter="get_navigation_visibility_mode" enum="TileMapLayer.DebugVisibilityMode" default="0"> Show or hide the [TileMapLayer]'s navigation meshes. If set to [constant DEBUG_VISIBILITY_MODE_DEFAULT], this depends on the show navigation debug settings. </member> + <member name="occlusion_enabled" type="bool" setter="set_occlusion_enabled" getter="is_occlusion_enabled" default="true"> + Enable or disable light occlusion. + </member> <member name="rendering_quadrant_size" type="int" setter="set_rendering_quadrant_size" getter="get_rendering_quadrant_size" default="16"> The [TileMapLayer]'s quadrant size. A quadrant is a group of tiles to be drawn together on a single canvas item, for optimization purposes. [member rendering_quadrant_size] defines the length of a square's side, in the map's coordinate system, that forms the quadrant. Thus, the default quadrant size groups together [code]16 * 16 = 256[/code] tiles. The quadrant size does not apply on a Y-sorted [TileMapLayer], as tiles are grouped by Y position instead in that case. diff --git a/doc/classes/Tween.xml b/doc/classes/Tween.xml index ac16bebecb..86a8130acc 100644 --- a/doc/classes/Tween.xml +++ b/doc/classes/Tween.xml @@ -183,7 +183,6 @@ <return type="Tween" /> <description> Makes the next [Tweener] run parallelly to the previous one. - [b]Example:[/b] [codeblocks] [gdscript] var tween = create_tween() @@ -410,7 +409,6 @@ <param index="3" name="duration" type="float" /> <description> Creates and appends a [PropertyTweener]. This method tweens a [param property] of an [param object] between an initial value and [param final_val] in a span of time equal to [param duration], in seconds. The initial value by default is the property's value at the time the tweening of the [PropertyTweener] starts. - [b]Example:[/b] [codeblocks] [gdscript] var tween = create_tween() diff --git a/doc/classes/Tweener.xml b/doc/classes/Tweener.xml index 65148e875d..88f5f9978c 100644 --- a/doc/classes/Tweener.xml +++ b/doc/classes/Tweener.xml @@ -11,7 +11,7 @@ <signals> <signal name="finished"> <description> - Emitted when the [Tweener] has just finished its job. + Emitted when the [Tweener] has just finished its job or became invalid (e.g. due to a freed object). </description> </signal> </signals> diff --git a/doc/classes/VehicleWheel3D.xml b/doc/classes/VehicleWheel3D.xml index 92b2297bf4..7b4952580c 100644 --- a/doc/classes/VehicleWheel3D.xml +++ b/doc/classes/VehicleWheel3D.xml @@ -18,6 +18,18 @@ Returns [code]null[/code] if the wheel is not in contact with a surface, or the contact body is not a [PhysicsBody3D]. </description> </method> + <method name="get_contact_normal" qualifiers="const"> + <return type="Vector3" /> + <description> + Returns the normal of the suspension's collision in world space if the wheel is in contact. If the wheel isn't in contact with anything, returns a vector pointing directly along the suspension axis toward the vehicle in world space. + </description> + </method> + <method name="get_contact_point" qualifiers="const"> + <return type="Vector3" /> + <description> + Returns the point of the suspension's collision in world space if the wheel is in contact. If the wheel isn't in contact with anything, returns the maximum point of the wheel's ray cast in world space, which is defined by [code]wheel_rest_length + wheel_radius[/code]. + </description> + </method> <method name="get_rpm" qualifiers="const"> <return type="float" /> <description> diff --git a/doc/classes/VisualShaderNodeCubemap.xml b/doc/classes/VisualShaderNodeCubemap.xml index 4e6cf2120a..6f3df9865a 100644 --- a/doc/classes/VisualShaderNodeCubemap.xml +++ b/doc/classes/VisualShaderNodeCubemap.xml @@ -9,7 +9,7 @@ <tutorials> </tutorials> <members> - <member name="cube_map" type="Cubemap" setter="set_cube_map" getter="get_cube_map"> + <member name="cube_map" type="TextureLayered" setter="set_cube_map" getter="get_cube_map"> The [Cubemap] texture to sample when using [constant SOURCE_TEXTURE] as [member source]. </member> <member name="source" type="int" setter="set_source" getter="get_source" enum="VisualShaderNodeCubemap.Source" default="0"> diff --git a/doc/classes/VisualShaderNodeTexture2DArray.xml b/doc/classes/VisualShaderNodeTexture2DArray.xml index 8852cb7cb4..bdf5a42821 100644 --- a/doc/classes/VisualShaderNodeTexture2DArray.xml +++ b/doc/classes/VisualShaderNodeTexture2DArray.xml @@ -9,7 +9,7 @@ <tutorials> </tutorials> <members> - <member name="texture_array" type="Texture2DArray" setter="set_texture_array" getter="get_texture_array"> + <member name="texture_array" type="TextureLayered" setter="set_texture_array" getter="get_texture_array"> A source texture array. Used if [member VisualShaderNodeSample3D.source] is set to [constant VisualShaderNodeSample3D.SOURCE_TEXTURE]. </member> </members> diff --git a/drivers/gles3/shaders/scene.glsl b/drivers/gles3/shaders/scene.glsl index ce2db7fa85..6143ce2167 100644 --- a/drivers/gles3/shaders/scene.glsl +++ b/drivers/gles3/shaders/scene.glsl @@ -44,6 +44,7 @@ LIGHTMAP_BICUBIC_FILTER = false #define M_PI 3.14159265359 #define SHADER_IS_SRGB true +#define SHADER_SPACE_FAR -1.0 #include "stdlib_inc.glsl" @@ -583,6 +584,7 @@ void main() { /* clang-format on */ #define SHADER_IS_SRGB true +#define SHADER_SPACE_FAR -1.0 #define FLAGS_NON_UNIFORM_SCALE (1 << 4) diff --git a/drivers/gles3/shaders/stdlib_inc.glsl b/drivers/gles3/shaders/stdlib_inc.glsl index 029084c34c..f88c218506 100644 --- a/drivers/gles3/shaders/stdlib_inc.glsl +++ b/drivers/gles3/shaders/stdlib_inc.glsl @@ -9,19 +9,17 @@ // Floating point pack/unpack functions are part of the GLSL ES 300 specification used by web and mobile. uint float2half(uint f) { - uint e = f & uint(0x7f800000); - if (e <= uint(0x38000000)) { - return uint(0); - } else { - return ((f >> uint(16)) & uint(0x8000)) | - (((e - uint(0x38000000)) >> uint(13)) & uint(0x7c00)) | - ((f >> uint(13)) & uint(0x03ff)); - } + uint b = f + uint(0x00001000); + uint e = (b & uint(0x7F800000)) >> 23; + uint m = b & uint(0x007FFFFF); + return (b & uint(0x80000000)) >> uint(16) | uint(e > uint(112)) * ((((e - uint(112)) << uint(10)) & uint(0x7C00)) | m >> uint(13)) | (uint(e < uint(113)) & uint(e > uint(101))) * ((((uint(0x007FF000) + m) >> (uint(125) - e)) + uint(1)) >> uint(1)) | uint(e > uint(143)) * uint(0x7FFF); } uint half2float(uint h) { - uint h_e = h & uint(0x7c00); - return ((h & uint(0x8000)) << uint(16)) | uint((h_e >> uint(10)) != uint(0)) * (((h_e + uint(0x1c000)) << uint(13)) | ((h & uint(0x03ff)) << uint(13))); + uint e = (h & uint(0x7C00)) >> uint(10); + uint m = (h & uint(0x03FF)) << uint(13); + uint v = m >> uint(23); + return (h & uint(0x8000)) << uint(16) | uint(e != uint(0)) * ((e + uint(112)) << uint(23) | m) | (uint(e == uint(0)) & uint(m != uint(0))) * ((v - uint(37)) << uint(23) | ((m << (uint(150) - v)) & uint(0x007FE000))); } uint godot_packHalf2x16(vec2 v) { diff --git a/drivers/gles3/storage/light_storage.h b/drivers/gles3/storage/light_storage.h index 81e7220439..ed00dd235f 100644 --- a/drivers/gles3/storage/light_storage.h +++ b/drivers/gles3/storage/light_storage.h @@ -203,7 +203,7 @@ struct LightmapInstance { class LightStorage : public RendererLightStorage { public: - enum ShadowAtlastQuadrant { + enum ShadowAtlastQuadrant : uint32_t { QUADRANT_SHIFT = 27, OMNI_LIGHT_FLAG = 1 << 26, SHADOW_INDEX_MASK = OMNI_LIGHT_FLAG - 1, diff --git a/drivers/gles3/storage/material_storage.cpp b/drivers/gles3/storage/material_storage.cpp index 25af7ff691..a37eba3b15 100644 --- a/drivers/gles3/storage/material_storage.cpp +++ b/drivers/gles3/storage/material_storage.cpp @@ -1273,6 +1273,7 @@ MaterialStorage::MaterialStorage() { actions.renames["CUSTOM2"] = "custom2_attrib"; actions.renames["CUSTOM3"] = "custom3_attrib"; actions.renames["OUTPUT_IS_SRGB"] = "SHADER_IS_SRGB"; + actions.renames["CLIP_SPACE_FAR"] = "SHADER_SPACE_FAR"; actions.renames["LIGHT_VERTEX"] = "light_vertex"; actions.renames["NODE_POSITION_WORLD"] = "model_matrix[3].xyz"; diff --git a/drivers/gles3/storage/mesh_storage.cpp b/drivers/gles3/storage/mesh_storage.cpp index b55a2e0a8a..de82d74aff 100644 --- a/drivers/gles3/storage/mesh_storage.cpp +++ b/drivers/gles3/storage/mesh_storage.cpp @@ -743,6 +743,7 @@ String MeshStorage::mesh_get_path(RID p_mesh) const { } void MeshStorage::mesh_set_shadow_mesh(RID p_mesh, RID p_shadow_mesh) { + ERR_FAIL_COND_MSG(p_mesh == p_shadow_mesh, "Cannot set a mesh as its own shadow mesh."); Mesh *mesh = mesh_owner.get_or_null(p_mesh); ERR_FAIL_NULL(mesh); diff --git a/drivers/gles3/storage/texture_storage.cpp b/drivers/gles3/storage/texture_storage.cpp index 8251c8f52e..36393dde86 100644 --- a/drivers/gles3/storage/texture_storage.cpp +++ b/drivers/gles3/storage/texture_storage.cpp @@ -1389,8 +1389,22 @@ void TextureStorage::texture_debug_usage(List<RS::TextureInfo> *r_info) { tinfo.format = t->format; tinfo.width = t->alloc_width; tinfo.height = t->alloc_height; - tinfo.depth = t->depth; tinfo.bytes = t->total_data_size; + + switch (t->type) { + case Texture::TYPE_3D: + tinfo.depth = t->depth; + break; + + case Texture::TYPE_LAYERED: + tinfo.depth = t->layers; + break; + + default: + tinfo.depth = 0; + break; + } + r_info->push_back(tinfo); } } @@ -1497,11 +1511,9 @@ void TextureStorage::_texture_set_data(RID p_texture, const Ref<Image> &p_image, glPixelStorei(GL_UNPACK_ALIGNMENT, 4); if (texture->target == GL_TEXTURE_2D_ARRAY) { if (p_initialize) { - glCompressedTexImage3D(GL_TEXTURE_2D_ARRAY, i, internal_format, w, h, texture->layers, 0, - size * texture->layers, &read[ofs]); - } else { - glCompressedTexSubImage3D(GL_TEXTURE_2D_ARRAY, i, 0, 0, p_layer, w, h, 1, internal_format, size, &read[ofs]); + glCompressedTexImage3D(GL_TEXTURE_2D_ARRAY, i, internal_format, w, h, texture->layers, 0, size * texture->layers, nullptr); } + glCompressedTexSubImage3D(GL_TEXTURE_2D_ARRAY, i, 0, 0, p_layer, w, h, 1, internal_format, size, &read[ofs]); } else { glCompressedTexImage2D(blit_target, i, internal_format, w, h, 0, size, &read[ofs]); } @@ -1523,7 +1535,11 @@ void TextureStorage::_texture_set_data(RID p_texture, const Ref<Image> &p_image, h = MAX(1, h >> 1); } - texture->total_data_size = tsize; + if (texture->target == GL_TEXTURE_CUBE_MAP || texture->target == GL_TEXTURE_2D_ARRAY) { + texture->total_data_size = tsize * texture->layers; + } else { + texture->total_data_size = tsize; + } texture->stored_cube_sides |= (1 << p_layer); diff --git a/drivers/metal/metal_objects.h b/drivers/metal/metal_objects.h index 11b96f8373..97f33bb1e8 100644 --- a/drivers/metal/metal_objects.h +++ b/drivers/metal/metal_objects.h @@ -227,6 +227,7 @@ public: id<MTLRenderCommandEncoder> encoder = nil; id<MTLBuffer> __unsafe_unretained index_buffer = nil; // Buffer is owned by RDD. MTLIndexType index_type = MTLIndexTypeUInt16; + uint32_t index_offset = 0; LocalVector<id<MTLBuffer> __unsafe_unretained> vertex_buffers; LocalVector<NSUInteger> vertex_offsets; // clang-format off @@ -506,10 +507,10 @@ enum class ShaderLoadStrategy { LAZY, }; -/** - * A Metal shader library. - */ -@interface MDLibrary : NSObject +/// A Metal shader library. +@interface MDLibrary : NSObject { + ShaderCacheEntry *_entry; +}; - (id<MTLLibrary>)library; - (NSError *)error; - (void)setLabel:(NSString *)label; @@ -536,6 +537,10 @@ struct SHA256Digest { SHA256Digest(const char *p_data, size_t p_length) { CC_SHA256(p_data, (CC_LONG)p_length, data); } + + _FORCE_INLINE_ uint32_t short_sha() const { + return __builtin_bswap32(*(uint32_t *)&data[0]); + } }; template <> @@ -545,22 +550,18 @@ struct HashMapComparatorDefault<SHA256Digest> { } }; -/** - * A cache entry for a Metal shader library. - */ +/// A cache entry for a Metal shader library. struct ShaderCacheEntry { RenderingDeviceDriverMetal &owner; + /// A hash of the Metal shader source code. SHA256Digest key; CharString name; - CharString short_sha; RD::ShaderStage stage = RD::SHADER_STAGE_VERTEX; - /** - * This reference must be weak, to ensure that when the last strong reference to the library - * is released, the cache entry is freed. - */ + /// This reference must be weak, to ensure that when the last strong reference to the library + /// is released, the cache entry is freed. MDLibrary *__weak library = nil; - /** Notify the cache that this entry is no longer needed. */ + /// Notify the cache that this entry is no longer needed. void notify_free() const; ShaderCacheEntry(RenderingDeviceDriverMetal &p_owner, SHA256Digest p_key) : diff --git a/drivers/metal/metal_objects.mm b/drivers/metal/metal_objects.mm index 5c7036f11e..abdcccf00c 100644 --- a/drivers/metal/metal_objects.mm +++ b/drivers/metal/metal_objects.mm @@ -717,6 +717,7 @@ void MDCommandBuffer::render_bind_index_buffer(RDD::BufferID p_buffer, RDD::Inde render.index_buffer = rid::get(p_buffer); render.index_type = p_format == RDD::IndexBufferFormat::INDEX_BUFFER_FORMAT_UINT16 ? MTLIndexTypeUInt16 : MTLIndexTypeUInt32; + render.index_offset = p_offset; } void MDCommandBuffer::render_draw_indexed(uint32_t p_index_count, @@ -729,13 +730,16 @@ void MDCommandBuffer::render_draw_indexed(uint32_t p_index_count, id<MTLRenderCommandEncoder> enc = render.encoder; + uint32_t index_offset = render.index_offset; + index_offset += p_first_index * (render.index_type == MTLIndexTypeUInt16 ? sizeof(uint16_t) : sizeof(uint32_t)); + [enc drawIndexedPrimitives:render.pipeline->raster_state.render_primitive indexCount:p_index_count indexType:render.index_type indexBuffer:render.index_buffer - indexBufferOffset:p_vertex_offset + indexBufferOffset:index_offset instanceCount:p_instance_count - baseVertex:p_first_index + baseVertex:p_vertex_offset baseInstance:p_first_instance]; } @@ -1396,9 +1400,9 @@ void ShaderCacheEntry::notify_free() const { @interface MDLibrary () - (instancetype)initWithCacheEntry:(ShaderCacheEntry *)entry; -- (ShaderCacheEntry *)entry; @end +/// Loads the MTLLibrary when the library is first accessed. @interface MDLazyLibrary : MDLibrary { id<MTLLibrary> _library; NSError *_error; @@ -1414,6 +1418,7 @@ void ShaderCacheEntry::notify_free() const { options:(MTLCompileOptions *)options; @end +/// Loads the MTLLibrary immediately on initialization, using an asynchronous API. @interface MDImmediateLibrary : MDLibrary { id<MTLLibrary> _library; NSError *_error; @@ -1428,9 +1433,7 @@ void ShaderCacheEntry::notify_free() const { options:(MTLCompileOptions *)options; @end -@implementation MDLibrary { - ShaderCacheEntry *_entry; -} +@implementation MDLibrary + (instancetype)newLibraryWithCacheEntry:(ShaderCacheEntry *)entry device:(id<MTLDevice>)device @@ -1447,10 +1450,6 @@ void ShaderCacheEntry::notify_free() const { } } -- (ShaderCacheEntry *)entry { - return _entry; -} - - (id<MTLLibrary>)library { CRASH_NOW_MSG("Not implemented"); return nil; @@ -1489,8 +1488,8 @@ void ShaderCacheEntry::notify_free() const { __block os_signpost_id_t compile_id = (os_signpost_id_t)(uintptr_t)self; os_signpost_interval_begin(LOG_INTERVALS, compile_id, "shader_compile", - "shader_name=%{public}s stage=%{public}s hash=%{public}s", - entry->name.get_data(), SHADER_STAGE_NAMES[entry->stage], entry->short_sha.get_data()); + "shader_name=%{public}s stage=%{public}s hash=%X", + entry->name.get_data(), SHADER_STAGE_NAMES[entry->stage], entry->key.short_sha()); [device newLibraryWithSource:source options:options @@ -1556,12 +1555,10 @@ void ShaderCacheEntry::notify_free() const { return; } - ShaderCacheEntry *entry = [self entry]; - __block os_signpost_id_t compile_id = (os_signpost_id_t)(uintptr_t)self; os_signpost_interval_begin(LOG_INTERVALS, compile_id, "shader_compile", - "shader_name=%{public}s stage=%{public}s hash=%{public}s", - entry->name.get_data(), SHADER_STAGE_NAMES[entry->stage], entry->short_sha.get_data()); + "shader_name=%{public}s stage=%{public}s hash=%X", + _entry->name.get_data(), SHADER_STAGE_NAMES[_entry->stage], _entry->key.short_sha()); NSError *error; _library = [_device newLibraryWithSource:_source options:_options error:&error]; os_signpost_interval_end(LOG_INTERVALS, compile_id, "shader_compile"); diff --git a/drivers/metal/rendering_device_driver_metal.mm b/drivers/metal/rendering_device_driver_metal.mm index ec42472cb6..9d691a0d23 100644 --- a/drivers/metal/rendering_device_driver_metal.mm +++ b/drivers/metal/rendering_device_driver_metal.mm @@ -72,8 +72,8 @@ os_log_t LOG_DRIVER; os_log_t LOG_INTERVALS; __attribute__((constructor)) static void InitializeLogging(void) { - LOG_DRIVER = os_log_create("org.stuartcarnie.godot.metal", OS_LOG_CATEGORY_POINTS_OF_INTEREST); - LOG_INTERVALS = os_log_create("org.stuartcarnie.godot.metal", "events"); + LOG_DRIVER = os_log_create("org.godotengine.godot.metal", OS_LOG_CATEGORY_POINTS_OF_INTEREST); + LOG_INTERVALS = os_log_create("org.godotengine.godot.metal", "events"); } /*****************/ @@ -2323,8 +2323,6 @@ RDD::ShaderID RenderingDeviceDriverMetal::shader_create_from_bytecode(const Vect ShaderCacheEntry *cd = memnew(ShaderCacheEntry(*this, key)); cd->name = binary_data.shader_name; - String sha_hex = String::hex_encode_buffer(key.data, CC_SHA256_DIGEST_LENGTH); - cd->short_sha = sha_hex.substr(0, 8).utf8(); cd->stage = shader_data.stage; MDLibrary *library = [MDLibrary newLibraryWithCacheEntry:cd @@ -3114,7 +3112,7 @@ RenderingDeviceDriverMetal::Result<id<MTLFunction>> RenderingDeviceDriverMetal:: NSArray<MTLFunctionConstant *> *constants = function.functionConstantsDictionary.allValues; bool is_sorted = true; for (uint32_t i = 1; i < constants.count; i++) { - if (constants[i - 1].index < constants[i].index) { + if (constants[i - 1].index > constants[i].index) { is_sorted = false; break; } diff --git a/drivers/unix/file_access_unix.cpp b/drivers/unix/file_access_unix.cpp index ea8b42b2e4..32f2d7dd79 100644 --- a/drivers/unix/file_access_unix.cpp +++ b/drivers/unix/file_access_unix.cpp @@ -218,67 +218,13 @@ bool FileAccessUnix::eof_reached() const { return last_error == ERR_FILE_EOF; } -uint8_t FileAccessUnix::get_8() const { - ERR_FAIL_NULL_V_MSG(f, 0, "File must be opened before use."); - uint8_t b; - if (fread(&b, 1, 1, f) == 0) { - check_errors(); - b = '\0'; - } - return b; -} - -uint16_t FileAccessUnix::get_16() const { - ERR_FAIL_NULL_V_MSG(f, 0, "File must be opened before use."); - - uint16_t b = 0; - if (fread(&b, 1, 2, f) != 2) { - check_errors(); - } - - if (big_endian) { - b = BSWAP16(b); - } - - return b; -} - -uint32_t FileAccessUnix::get_32() const { - ERR_FAIL_NULL_V_MSG(f, 0, "File must be opened before use."); - - uint32_t b = 0; - if (fread(&b, 1, 4, f) != 4) { - check_errors(); - } - - if (big_endian) { - b = BSWAP32(b); - } - - return b; -} - -uint64_t FileAccessUnix::get_64() const { - ERR_FAIL_NULL_V_MSG(f, 0, "File must be opened before use."); - - uint64_t b = 0; - if (fread(&b, 1, 8, f) != 8) { - check_errors(); - } - - if (big_endian) { - b = BSWAP64(b); - } - - return b; -} - uint64_t FileAccessUnix::get_buffer(uint8_t *p_dst, uint64_t p_length) const { - ERR_FAIL_COND_V(!p_dst && p_length > 0, -1); ERR_FAIL_NULL_V_MSG(f, -1, "File must be opened before use."); + ERR_FAIL_COND_V(!p_dst && p_length > 0, -1); uint64_t read = fread(p_dst, 1, p_length, f); check_errors(); + return read; } @@ -308,41 +254,6 @@ void FileAccessUnix::flush() { fflush(f); } -void FileAccessUnix::store_8(uint8_t p_dest) { - ERR_FAIL_NULL_MSG(f, "File must be opened before use."); - ERR_FAIL_COND(fwrite(&p_dest, 1, 1, f) != 1); -} - -void FileAccessUnix::store_16(uint16_t p_dest) { - ERR_FAIL_NULL_MSG(f, "File must be opened before use."); - - if (big_endian) { - p_dest = BSWAP16(p_dest); - } - - ERR_FAIL_COND(fwrite(&p_dest, 1, 2, f) != 2); -} - -void FileAccessUnix::store_32(uint32_t p_dest) { - ERR_FAIL_NULL_MSG(f, "File must be opened before use."); - - if (big_endian) { - p_dest = BSWAP32(p_dest); - } - - ERR_FAIL_COND(fwrite(&p_dest, 1, 4, f) != 4); -} - -void FileAccessUnix::store_64(uint64_t p_dest) { - ERR_FAIL_NULL_MSG(f, "File must be opened before use."); - - if (big_endian) { - p_dest = BSWAP64(p_dest); - } - - ERR_FAIL_COND(fwrite(&p_dest, 1, 8, f) != 8); -} - void FileAccessUnix::store_buffer(const uint8_t *p_src, uint64_t p_length) { ERR_FAIL_NULL_MSG(f, "File must be opened before use."); ERR_FAIL_COND(!p_src && p_length > 0); diff --git a/drivers/unix/file_access_unix.h b/drivers/unix/file_access_unix.h index c0286dbff3..76f629f7c2 100644 --- a/drivers/unix/file_access_unix.h +++ b/drivers/unix/file_access_unix.h @@ -67,20 +67,12 @@ public: virtual bool eof_reached() const override; ///< reading passed EOF - virtual uint8_t get_8() const override; ///< get a byte - virtual uint16_t get_16() const override; - virtual uint32_t get_32() const override; - virtual uint64_t get_64() const override; virtual uint64_t get_buffer(uint8_t *p_dst, uint64_t p_length) const override; virtual Error get_error() const override; ///< get last error virtual Error resize(int64_t p_length) override; virtual void flush() override; - virtual void store_8(uint8_t p_dest) override; ///< store a byte - virtual void store_16(uint16_t p_dest) override; - virtual void store_32(uint32_t p_dest) override; - virtual void store_64(uint64_t p_dest) override; virtual void store_buffer(const uint8_t *p_src, uint64_t p_length) override; ///< store an array of bytes virtual bool file_exists(const String &p_path) override; ///< return true if a file exists diff --git a/drivers/unix/file_access_unix_pipe.cpp b/drivers/unix/file_access_unix_pipe.cpp index 5d9a27ad05..34758e8c7d 100644 --- a/drivers/unix/file_access_unix_pipe.cpp +++ b/drivers/unix/file_access_unix_pipe.cpp @@ -125,22 +125,9 @@ String FileAccessUnixPipe::get_path_absolute() const { return path_src; } -uint8_t FileAccessUnixPipe::get_8() const { - ERR_FAIL_COND_V_MSG(fd[0] < 0, 0, "Pipe must be opened before use."); - - uint8_t b; - if (::read(fd[0], &b, 1) == 0) { - last_error = ERR_FILE_CANT_READ; - b = '\0'; - } else { - last_error = OK; - } - return b; -} - uint64_t FileAccessUnixPipe::get_buffer(uint8_t *p_dst, uint64_t p_length) const { - ERR_FAIL_COND_V(!p_dst && p_length > 0, -1); ERR_FAIL_COND_V_MSG(fd[0] < 0, -1, "Pipe must be opened before use."); + ERR_FAIL_COND_V(!p_dst && p_length > 0, -1); uint64_t read = ::read(fd[0], p_dst, p_length); if (read == p_length) { @@ -155,18 +142,10 @@ Error FileAccessUnixPipe::get_error() const { return last_error; } -void FileAccessUnixPipe::store_8(uint8_t p_src) { - ERR_FAIL_COND_MSG(fd[1] < 0, "Pipe must be opened before use."); - if (::write(fd[1], &p_src, 1) != 1) { - last_error = ERR_FILE_CANT_WRITE; - } else { - last_error = OK; - } -} - void FileAccessUnixPipe::store_buffer(const uint8_t *p_src, uint64_t p_length) { ERR_FAIL_COND_MSG(fd[1] < 0, "Pipe must be opened before use."); ERR_FAIL_COND(!p_src && p_length > 0); + if (::write(fd[1], p_src, p_length) != (ssize_t)p_length) { last_error = ERR_FILE_CANT_WRITE; } else { diff --git a/drivers/unix/file_access_unix_pipe.h b/drivers/unix/file_access_unix_pipe.h index 8e7988791b..19acdb5a37 100644 --- a/drivers/unix/file_access_unix_pipe.h +++ b/drivers/unix/file_access_unix_pipe.h @@ -65,14 +65,12 @@ public: virtual bool eof_reached() const override { return false; } - virtual uint8_t get_8() const override; ///< get a byte virtual uint64_t get_buffer(uint8_t *p_dst, uint64_t p_length) const override; virtual Error get_error() const override; ///< get last error virtual Error resize(int64_t p_length) override { return ERR_UNAVAILABLE; } virtual void flush() override {} - virtual void store_8(uint8_t p_src) override; ///< store a byte virtual void store_buffer(const uint8_t *p_src, uint64_t p_length) override; ///< store an array of bytes virtual bool file_exists(const String &p_path) override { return false; } diff --git a/drivers/vulkan/rendering_device_driver_vulkan.cpp b/drivers/vulkan/rendering_device_driver_vulkan.cpp index 2ba353868b..4ea46e8214 100644 --- a/drivers/vulkan/rendering_device_driver_vulkan.cpp +++ b/drivers/vulkan/rendering_device_driver_vulkan.cpp @@ -497,6 +497,7 @@ Error RenderingDeviceDriverVulkan::_initialize_device_extensions() { _register_requested_device_extension(VK_KHR_MAINTENANCE_2_EXTENSION_NAME, false); _register_requested_device_extension(VK_EXT_PIPELINE_CREATION_CACHE_CONTROL_EXTENSION_NAME, false); _register_requested_device_extension(VK_EXT_SUBGROUP_SIZE_CONTROL_EXTENSION_NAME, false); + _register_requested_device_extension(VK_EXT_ASTC_DECODE_MODE_EXTENSION_NAME, false); if (Engine::get_singleton()->is_generate_spirv_debug_info_enabled()) { _register_requested_device_extension(VK_KHR_SHADER_NON_SEMANTIC_INFO_EXTENSION_NAME, true); @@ -1705,6 +1706,16 @@ RDD::TextureID RenderingDeviceDriverVulkan::texture_create(const TextureFormat & image_view_create_info.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; } + VkImageViewASTCDecodeModeEXT decode_mode; + if (enabled_device_extension_names.has(VK_EXT_ASTC_DECODE_MODE_EXTENSION_NAME)) { + if (image_view_create_info.format >= VK_FORMAT_ASTC_4x4_UNORM_BLOCK && image_view_create_info.format <= VK_FORMAT_ASTC_12x12_SRGB_BLOCK) { + decode_mode.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_ASTC_DECODE_MODE_EXT; + decode_mode.pNext = nullptr; + decode_mode.decodeMode = VK_FORMAT_R8G8B8A8_UNORM; + image_view_create_info.pNext = &decode_mode; + } + } + VkImageView vk_image_view = VK_NULL_HANDLE; err = vkCreateImageView(vk_device, &image_view_create_info, VKC::get_allocation_callbacks(VK_OBJECT_TYPE_IMAGE_VIEW), &vk_image_view); if (err) { diff --git a/drivers/windows/dir_access_windows.cpp b/drivers/windows/dir_access_windows.cpp index 63ba6a6c96..f7632842ed 100644 --- a/drivers/windows/dir_access_windows.cpp +++ b/drivers/windows/dir_access_windows.cpp @@ -35,6 +35,7 @@ #include "core/config/project_settings.h" #include "core/os/memory.h" +#include "core/os/os.h" #include "core/string/print_string.h" #include <stdio.h> @@ -69,9 +70,19 @@ struct DirAccessWindowsPrivate { }; String DirAccessWindows::fix_path(const String &p_path) const { - String r_path = DirAccess::fix_path(p_path); - if (r_path.is_absolute_path() && !r_path.is_network_share_path() && r_path.length() > MAX_PATH) { - r_path = "\\\\?\\" + r_path.replace("/", "\\"); + String r_path = DirAccess::fix_path(p_path.trim_prefix(R"(\\?\)").replace("\\", "/")); + if (r_path.ends_with(":")) { + r_path += "/"; + } + if (r_path.is_relative_path()) { + r_path = current_dir.trim_prefix(R"(\\?\)").replace("\\", "/").path_join(r_path); + } else if (r_path == ".") { + r_path = current_dir.trim_prefix(R"(\\?\)").replace("\\", "/"); + } + r_path = r_path.simplify_path(); + r_path = r_path.replace("/", "\\"); + if (!r_path.is_network_share_path() && !r_path.begins_with(R"(\\?\)")) { + r_path = R"(\\?\)" + r_path; } return r_path; } @@ -140,28 +151,33 @@ String DirAccessWindows::get_drive(int p_drive) { Error DirAccessWindows::change_dir(String p_dir) { GLOBAL_LOCK_FUNCTION - p_dir = fix_path(p_dir); + String dir = fix_path(p_dir); - WCHAR real_current_dir_name[2048]; - GetCurrentDirectoryW(2048, real_current_dir_name); - String prev_dir = String::utf16((const char16_t *)real_current_dir_name); + Char16String real_current_dir_name; + size_t str_len = GetCurrentDirectoryW(0, nullptr); + real_current_dir_name.resize(str_len + 1); + GetCurrentDirectoryW(real_current_dir_name.size(), (LPWSTR)real_current_dir_name.ptrw()); + String prev_dir = String::utf16((const char16_t *)real_current_dir_name.get_data()); SetCurrentDirectoryW((LPCWSTR)(current_dir.utf16().get_data())); - bool worked = (SetCurrentDirectoryW((LPCWSTR)(p_dir.utf16().get_data())) != 0); + bool worked = (SetCurrentDirectoryW((LPCWSTR)(dir.utf16().get_data())) != 0); String base = _get_root_path(); if (!base.is_empty()) { - GetCurrentDirectoryW(2048, real_current_dir_name); - String new_dir = String::utf16((const char16_t *)real_current_dir_name).replace("\\", "/"); + str_len = GetCurrentDirectoryW(0, nullptr); + real_current_dir_name.resize(str_len + 1); + GetCurrentDirectoryW(real_current_dir_name.size(), (LPWSTR)real_current_dir_name.ptrw()); + String new_dir = String::utf16((const char16_t *)real_current_dir_name.get_data()).trim_prefix(R"(\\?\)").replace("\\", "/"); if (!new_dir.begins_with(base)) { worked = false; } } if (worked) { - GetCurrentDirectoryW(2048, real_current_dir_name); - current_dir = String::utf16((const char16_t *)real_current_dir_name); - current_dir = current_dir.replace("\\", "/"); + str_len = GetCurrentDirectoryW(0, nullptr); + real_current_dir_name.resize(str_len + 1); + GetCurrentDirectoryW(real_current_dir_name.size(), (LPWSTR)real_current_dir_name.ptrw()); + current_dir = String::utf16((const char16_t *)real_current_dir_name.get_data()); } SetCurrentDirectoryW((LPCWSTR)(prev_dir.utf16().get_data())); @@ -172,12 +188,6 @@ Error DirAccessWindows::change_dir(String p_dir) { Error DirAccessWindows::make_dir(String p_dir) { GLOBAL_LOCK_FUNCTION - p_dir = fix_path(p_dir); - if (p_dir.is_relative_path()) { - p_dir = current_dir.path_join(p_dir); - p_dir = fix_path(p_dir); - } - if (FileAccessWindows::is_path_invalid(p_dir)) { #ifdef DEBUG_ENABLED WARN_PRINT("The path :" + p_dir + " is a reserved Windows system pipe, so it can't be used for creating directories."); @@ -185,12 +195,12 @@ Error DirAccessWindows::make_dir(String p_dir) { return ERR_INVALID_PARAMETER; } - p_dir = p_dir.simplify_path().replace("/", "\\"); + String dir = fix_path(p_dir); bool success; int err; - success = CreateDirectoryW((LPCWSTR)(p_dir.utf16().get_data()), nullptr); + success = CreateDirectoryW((LPCWSTR)(dir.utf16().get_data()), nullptr); err = GetLastError(); if (success) { @@ -205,9 +215,10 @@ Error DirAccessWindows::make_dir(String p_dir) { } String DirAccessWindows::get_current_dir(bool p_include_drive) const { + String cdir = current_dir.trim_prefix(R"(\\?\)").replace("\\", "/"); String base = _get_root_path(); if (!base.is_empty()) { - String bd = current_dir.replace("\\", "/").replace_first(base, ""); + String bd = cdir.replace_first(base, ""); if (bd.begins_with("/")) { return _get_root_string() + bd.substr(1, bd.length()); } else { @@ -216,30 +227,25 @@ String DirAccessWindows::get_current_dir(bool p_include_drive) const { } if (p_include_drive) { - return current_dir; + return cdir; } else { if (_get_root_string().is_empty()) { - int pos = current_dir.find(":"); + int pos = cdir.find(":"); if (pos != -1) { - return current_dir.substr(pos + 1); + return cdir.substr(pos + 1); } } - return current_dir; + return cdir; } } bool DirAccessWindows::file_exists(String p_file) { GLOBAL_LOCK_FUNCTION - if (!p_file.is_absolute_path()) { - p_file = get_current_dir().path_join(p_file); - } - - p_file = fix_path(p_file); + String file = fix_path(p_file); DWORD fileAttr; - - fileAttr = GetFileAttributesW((LPCWSTR)(p_file.utf16().get_data())); + fileAttr = GetFileAttributesW((LPCWSTR)(file.utf16().get_data())); if (INVALID_FILE_ATTRIBUTES == fileAttr) { return false; } @@ -250,14 +256,10 @@ bool DirAccessWindows::file_exists(String p_file) { bool DirAccessWindows::dir_exists(String p_dir) { GLOBAL_LOCK_FUNCTION - if (p_dir.is_relative_path()) { - p_dir = get_current_dir().path_join(p_dir); - } - - p_dir = fix_path(p_dir); + String dir = fix_path(p_dir); DWORD fileAttr; - fileAttr = GetFileAttributesW((LPCWSTR)(p_dir.utf16().get_data())); + fileAttr = GetFileAttributesW((LPCWSTR)(dir.utf16().get_data())); if (INVALID_FILE_ATTRIBUTES == fileAttr) { return false; } @@ -265,66 +267,63 @@ bool DirAccessWindows::dir_exists(String p_dir) { } Error DirAccessWindows::rename(String p_path, String p_new_path) { - if (p_path.is_relative_path()) { - p_path = get_current_dir().path_join(p_path); - } - - p_path = fix_path(p_path); - - if (p_new_path.is_relative_path()) { - p_new_path = get_current_dir().path_join(p_new_path); - } - - p_new_path = fix_path(p_new_path); + String path = fix_path(p_path); + String new_path = fix_path(p_new_path); // If we're only changing file name case we need to do a little juggling - if (p_path.to_lower() == p_new_path.to_lower()) { - if (dir_exists(p_path)) { + if (path.to_lower() == new_path.to_lower()) { + if (dir_exists(path)) { // The path is a dir; just rename - return ::_wrename((LPCWSTR)(p_path.utf16().get_data()), (LPCWSTR)(p_new_path.utf16().get_data())) == 0 ? OK : FAILED; + return MoveFileW((LPCWSTR)(path.utf16().get_data()), (LPCWSTR)(new_path.utf16().get_data())) != 0 ? OK : FAILED; } // The path is a file; juggle - WCHAR tmpfile[MAX_PATH]; - - if (!GetTempFileNameW((LPCWSTR)(fix_path(get_current_dir()).utf16().get_data()), nullptr, 0, tmpfile)) { - return FAILED; + // Note: do not use GetTempFileNameW, it's not long path aware! + Char16String tmpfile_utf16; + uint64_t id = OS::get_singleton()->get_ticks_usec(); + while (true) { + tmpfile_utf16 = (path + itos(id++) + ".tmp").utf16(); + HANDLE handle = CreateFileW((LPCWSTR)tmpfile_utf16.get_data(), GENERIC_WRITE, 0, NULL, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, 0); + if (handle != INVALID_HANDLE_VALUE) { + CloseHandle(handle); + break; + } + if (GetLastError() != ERROR_FILE_EXISTS && GetLastError() != ERROR_SHARING_VIOLATION) { + return FAILED; + } } - if (!::ReplaceFileW(tmpfile, (LPCWSTR)(p_path.utf16().get_data()), nullptr, 0, nullptr, nullptr)) { - DeleteFileW(tmpfile); + if (!::ReplaceFileW((LPCWSTR)tmpfile_utf16.get_data(), (LPCWSTR)(path.utf16().get_data()), nullptr, 0, nullptr, nullptr)) { + DeleteFileW((LPCWSTR)tmpfile_utf16.get_data()); return FAILED; } - return ::_wrename(tmpfile, (LPCWSTR)(p_new_path.utf16().get_data())) == 0 ? OK : FAILED; + return MoveFileW((LPCWSTR)tmpfile_utf16.get_data(), (LPCWSTR)(new_path.utf16().get_data())) != 0 ? OK : FAILED; } else { - if (file_exists(p_new_path)) { - if (remove(p_new_path) != OK) { + if (file_exists(new_path)) { + if (remove(new_path) != OK) { return FAILED; } } - return ::_wrename((LPCWSTR)(p_path.utf16().get_data()), (LPCWSTR)(p_new_path.utf16().get_data())) == 0 ? OK : FAILED; + return MoveFileW((LPCWSTR)(path.utf16().get_data()), (LPCWSTR)(new_path.utf16().get_data())) != 0 ? OK : FAILED; } } Error DirAccessWindows::remove(String p_path) { - if (p_path.is_relative_path()) { - p_path = get_current_dir().path_join(p_path); - } - - p_path = fix_path(p_path); + String path = fix_path(p_path); + const Char16String &path_utf16 = path.utf16(); DWORD fileAttr; - fileAttr = GetFileAttributesW((LPCWSTR)(p_path.utf16().get_data())); + fileAttr = GetFileAttributesW((LPCWSTR)(path_utf16.get_data())); if (INVALID_FILE_ATTRIBUTES == fileAttr) { return FAILED; } if ((fileAttr & FILE_ATTRIBUTE_DIRECTORY)) { - return ::_wrmdir((LPCWSTR)(p_path.utf16().get_data())) == 0 ? OK : FAILED; + return RemoveDirectoryW((LPCWSTR)(path_utf16.get_data())) != 0 ? OK : FAILED; } else { - return ::_wunlink((LPCWSTR)(p_path.utf16().get_data())) == 0 ? OK : FAILED; + return DeleteFileW((LPCWSTR)(path_utf16.get_data())) != 0 ? OK : FAILED; } } @@ -339,16 +338,16 @@ uint64_t DirAccessWindows::get_space_left() { } String DirAccessWindows::get_filesystem_type() const { - String path = fix_path(const_cast<DirAccessWindows *>(this)->get_current_dir()); - - int unit_end = path.find(":"); - ERR_FAIL_COND_V(unit_end == -1, String()); - String unit = path.substr(0, unit_end + 1) + "\\"; + String path = current_dir.trim_prefix(R"(\\?\)"); if (path.is_network_share_path()) { return "Network Share"; } + int unit_end = path.find(":"); + ERR_FAIL_COND_V(unit_end == -1, String()); + String unit = path.substr(0, unit_end + 1) + "\\"; + WCHAR szVolumeName[100]; WCHAR szFileSystemName[10]; DWORD dwSerialNumber = 0; @@ -370,11 +369,7 @@ String DirAccessWindows::get_filesystem_type() const { } bool DirAccessWindows::is_case_sensitive(const String &p_path) const { - String f = p_path; - if (!f.is_absolute_path()) { - f = get_current_dir().path_join(f); - } - f = fix_path(f); + String f = fix_path(p_path); HANDLE h_file = ::CreateFileW((LPCWSTR)(f.utf16().get_data()), 0, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, @@ -397,12 +392,7 @@ bool DirAccessWindows::is_case_sensitive(const String &p_path) const { } bool DirAccessWindows::is_link(String p_file) { - String f = p_file; - - if (!f.is_absolute_path()) { - f = get_current_dir().path_join(f); - } - f = fix_path(f); + String f = fix_path(p_file); DWORD attr = GetFileAttributesW((LPCWSTR)(f.utf16().get_data())); if (attr == INVALID_FILE_ATTRIBUTES) { @@ -413,12 +403,7 @@ bool DirAccessWindows::is_link(String p_file) { } String DirAccessWindows::read_link(String p_file) { - String f = p_file; - - if (!f.is_absolute_path()) { - f = get_current_dir().path_join(f); - } - f = fix_path(f); + String f = fix_path(p_file); HANDLE hfile = CreateFileW((LPCWSTR)(f.utf16().get_data()), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); if (hfile == INVALID_HANDLE_VALUE) { @@ -434,22 +419,18 @@ String DirAccessWindows::read_link(String p_file) { GetFinalPathNameByHandleW(hfile, (LPWSTR)cs.ptrw(), ret, VOLUME_NAME_DOS | FILE_NAME_NORMALIZED); CloseHandle(hfile); - return String::utf16((const char16_t *)cs.ptr(), ret).trim_prefix(R"(\\?\)"); + return String::utf16((const char16_t *)cs.ptr(), ret).trim_prefix(R"(\\?\)").replace("\\", "/"); } Error DirAccessWindows::create_link(String p_source, String p_target) { - if (p_target.is_relative_path()) { - p_target = get_current_dir().path_join(p_target); - } + String source = fix_path(p_source); + String target = fix_path(p_target); - p_source = fix_path(p_source); - p_target = fix_path(p_target); - - DWORD file_attr = GetFileAttributesW((LPCWSTR)(p_source.utf16().get_data())); + DWORD file_attr = GetFileAttributesW((LPCWSTR)(source.utf16().get_data())); bool is_dir = (file_attr & FILE_ATTRIBUTE_DIRECTORY); DWORD flags = ((is_dir) ? SYMBOLIC_LINK_FLAG_DIRECTORY : 0) | SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE; - if (CreateSymbolicLinkW((LPCWSTR)p_target.utf16().get_data(), (LPCWSTR)p_source.utf16().get_data(), flags) != 0) { + if (CreateSymbolicLinkW((LPCWSTR)target.utf16().get_data(), (LPCWSTR)source.utf16().get_data(), flags) != 0) { return OK; } else { return FAILED; @@ -459,7 +440,12 @@ Error DirAccessWindows::create_link(String p_source, String p_target) { DirAccessWindows::DirAccessWindows() { p = memnew(DirAccessWindowsPrivate); p->h = INVALID_HANDLE_VALUE; - current_dir = "."; + + Char16String real_current_dir_name; + size_t str_len = GetCurrentDirectoryW(0, nullptr); + real_current_dir_name.resize(str_len + 1); + GetCurrentDirectoryW(real_current_dir_name.size(), (LPWSTR)real_current_dir_name.ptrw()); + current_dir = String::utf16((const char16_t *)real_current_dir_name.get_data()); DWORD mask = GetLogicalDrives(); diff --git a/drivers/windows/file_access_windows.cpp b/drivers/windows/file_access_windows.cpp index 9885d9d7ee..0243d863f8 100644 --- a/drivers/windows/file_access_windows.cpp +++ b/drivers/windows/file_access_windows.cpp @@ -73,8 +73,18 @@ bool FileAccessWindows::is_path_invalid(const String &p_path) { String FileAccessWindows::fix_path(const String &p_path) const { String r_path = FileAccess::fix_path(p_path); - if (r_path.is_absolute_path() && !r_path.is_network_share_path() && r_path.length() > MAX_PATH) { - r_path = "\\\\?\\" + r_path.replace("/", "\\"); + + if (r_path.is_relative_path()) { + Char16String current_dir_name; + size_t str_len = GetCurrentDirectoryW(0, nullptr); + current_dir_name.resize(str_len + 1); + GetCurrentDirectoryW(current_dir_name.size(), (LPWSTR)current_dir_name.ptrw()); + r_path = String::utf16((const char16_t *)current_dir_name.get_data()).trim_prefix(R"(\\?\)").replace("\\", "/").path_join(r_path); + } + r_path = r_path.simplify_path(); + r_path = r_path.replace("/", "\\"); + if (!r_path.is_network_share_path() && !r_path.begins_with(R"(\\?\)")) { + r_path = R"(\\?\)" + r_path; } return r_path; } @@ -108,9 +118,6 @@ Error FileAccessWindows::open_internal(const String &p_path, int p_mode_flags) { return ERR_INVALID_PARAMETER; } - /* Pretty much every implementation that uses fopen as primary - backend supports utf8 encoding. */ - struct _stat st; if (_wstat((LPCWSTR)(path.utf16().get_data()), &st) == 0) { if (!S_ISREG(st.st_mode)) { @@ -125,7 +132,7 @@ Error FileAccessWindows::open_internal(const String &p_path, int p_mode_flags) { // platforms), we only check for relative paths, or paths in res:// or user://, // other paths aren't likely to be portable anyway. if (p_mode_flags == READ && (p_path.is_relative_path() || get_access_type() != ACCESS_FILESYSTEM)) { - String base_path = path; + String base_path = p_path; String working_path; String proper_path; @@ -144,23 +151,17 @@ Error FileAccessWindows::open_internal(const String &p_path, int p_mode_flags) { } proper_path = "user://"; } + working_path = fix_path(working_path); WIN32_FIND_DATAW d; - Vector<String> parts = base_path.split("/"); + Vector<String> parts = base_path.simplify_path().split("/"); bool mismatch = false; for (const String &part : parts) { - working_path = working_path.path_join(part); - - // Skip if relative. - if (part == "." || part == "..") { - proper_path = proper_path.path_join(part); - continue; - } + working_path = working_path + "\\" + part; HANDLE fnd = FindFirstFileW((LPCWSTR)(working_path.utf16().get_data()), &d); - if (fnd == INVALID_HANDLE_VALUE) { mismatch = false; break; @@ -186,12 +187,22 @@ Error FileAccessWindows::open_internal(const String &p_path, int p_mode_flags) { if (is_backup_save_enabled() && p_mode_flags == WRITE) { save_path = path; // Create a temporary file in the same directory as the target file. - WCHAR tmpFileName[MAX_PATH]; - if (GetTempFileNameW((LPCWSTR)(path.get_base_dir().utf16().get_data()), (LPCWSTR)(path.get_file().utf16().get_data()), 0, tmpFileName) == 0) { - last_error = ERR_FILE_CANT_OPEN; - return last_error; + // Note: do not use GetTempFileNameW, it's not long path aware! + String tmpfile; + uint64_t id = OS::get_singleton()->get_ticks_usec(); + while (true) { + tmpfile = path + itos(id++) + ".tmp"; + HANDLE handle = CreateFileW((LPCWSTR)tmpfile.utf16().get_data(), GENERIC_WRITE, 0, NULL, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, 0); + if (handle != INVALID_HANDLE_VALUE) { + CloseHandle(handle); + break; + } + if (GetLastError() != ERROR_FILE_EXISTS && GetLastError() != ERROR_SHARING_VIOLATION) { + last_error = ERR_FILE_CANT_WRITE; + return FAILED; + } } - path = tmpFileName; + path = tmpfile; } f = _wfsopen((LPCWSTR)(path.utf16().get_data()), mode_string, is_backup_save_enabled() ? _SH_SECURE : _SH_DENYNO); @@ -235,7 +246,7 @@ void FileAccessWindows::_close() { } else { // Either the target exists and is locked (temporarily, hopefully) // or it doesn't exist; let's assume the latter before re-trying. - rename_error = _wrename((LPCWSTR)(path_utf16.get_data()), (LPCWSTR)(save_path_utf16.get_data())) != 0; + rename_error = MoveFileW((LPCWSTR)(path_utf16.get_data()), (LPCWSTR)(save_path_utf16.get_data())) == 0; } if (!rename_error) { @@ -262,7 +273,7 @@ String FileAccessWindows::get_path() const { } String FileAccessWindows::get_path_absolute() const { - return path; + return path.trim_prefix(R"(\\?\)").replace("\\", "/"); } bool FileAccessWindows::is_open() const { @@ -312,93 +323,9 @@ bool FileAccessWindows::eof_reached() const { return last_error == ERR_FILE_EOF; } -uint8_t FileAccessWindows::get_8() const { - ERR_FAIL_NULL_V(f, 0); - - if (flags == READ_WRITE || flags == WRITE_READ) { - if (prev_op == WRITE) { - fflush(f); - } - prev_op = READ; - } - uint8_t b; - if (fread(&b, 1, 1, f) == 0) { - check_errors(); - b = '\0'; - } - - return b; -} - -uint16_t FileAccessWindows::get_16() const { - ERR_FAIL_NULL_V(f, 0); - - if (flags == READ_WRITE || flags == WRITE_READ) { - if (prev_op == WRITE) { - fflush(f); - } - prev_op = READ; - } - - uint16_t b = 0; - if (fread(&b, 1, 2, f) != 2) { - check_errors(); - } - - if (big_endian) { - b = BSWAP16(b); - } - - return b; -} - -uint32_t FileAccessWindows::get_32() const { - ERR_FAIL_NULL_V(f, 0); - - if (flags == READ_WRITE || flags == WRITE_READ) { - if (prev_op == WRITE) { - fflush(f); - } - prev_op = READ; - } - - uint32_t b = 0; - if (fread(&b, 1, 4, f) != 4) { - check_errors(); - } - - if (big_endian) { - b = BSWAP32(b); - } - - return b; -} - -uint64_t FileAccessWindows::get_64() const { - ERR_FAIL_NULL_V(f, 0); - - if (flags == READ_WRITE || flags == WRITE_READ) { - if (prev_op == WRITE) { - fflush(f); - } - prev_op = READ; - } - - uint64_t b = 0; - if (fread(&b, 1, 8, f) != 8) { - check_errors(); - } - - if (big_endian) { - b = BSWAP64(b); - } - - return b; -} - uint64_t FileAccessWindows::get_buffer(uint8_t *p_dst, uint64_t p_length) const { - ERR_FAIL_COND_V(!p_dst && p_length > 0, -1); ERR_FAIL_NULL_V(f, -1); + ERR_FAIL_COND_V(!p_dst && p_length > 0, -1); if (flags == READ_WRITE || flags == WRITE_READ) { if (prev_op == WRITE) { @@ -406,8 +333,10 @@ uint64_t FileAccessWindows::get_buffer(uint8_t *p_dst, uint64_t p_length) const } prev_op = READ; } + uint64_t read = fread(p_dst, 1, p_length, f); check_errors(); + return read; } @@ -442,77 +371,6 @@ void FileAccessWindows::flush() { } } -void FileAccessWindows::store_8(uint8_t p_dest) { - ERR_FAIL_NULL(f); - - if (flags == READ_WRITE || flags == WRITE_READ) { - if (prev_op == READ) { - if (last_error != ERR_FILE_EOF) { - fseek(f, 0, SEEK_CUR); - } - } - prev_op = WRITE; - } - fwrite(&p_dest, 1, 1, f); -} - -void FileAccessWindows::store_16(uint16_t p_dest) { - ERR_FAIL_NULL(f); - - if (flags == READ_WRITE || flags == WRITE_READ) { - if (prev_op == READ) { - if (last_error != ERR_FILE_EOF) { - fseek(f, 0, SEEK_CUR); - } - } - prev_op = WRITE; - } - - if (big_endian) { - p_dest = BSWAP16(p_dest); - } - - fwrite(&p_dest, 1, 2, f); -} - -void FileAccessWindows::store_32(uint32_t p_dest) { - ERR_FAIL_NULL(f); - - if (flags == READ_WRITE || flags == WRITE_READ) { - if (prev_op == READ) { - if (last_error != ERR_FILE_EOF) { - fseek(f, 0, SEEK_CUR); - } - } - prev_op = WRITE; - } - - if (big_endian) { - p_dest = BSWAP32(p_dest); - } - - fwrite(&p_dest, 1, 4, f); -} - -void FileAccessWindows::store_64(uint64_t p_dest) { - ERR_FAIL_NULL(f); - - if (flags == READ_WRITE || flags == WRITE_READ) { - if (prev_op == READ) { - if (last_error != ERR_FILE_EOF) { - fseek(f, 0, SEEK_CUR); - } - } - prev_op = WRITE; - } - - if (big_endian) { - p_dest = BSWAP64(p_dest); - } - - fwrite(&p_dest, 1, 8, f); -} - void FileAccessWindows::store_buffer(const uint8_t *p_src, uint64_t p_length) { ERR_FAIL_NULL(f); ERR_FAIL_COND(!p_src && p_length > 0); @@ -525,6 +383,7 @@ void FileAccessWindows::store_buffer(const uint8_t *p_src, uint64_t p_length) { } prev_op = WRITE; } + ERR_FAIL_COND(fwrite(p_src, 1, p_length, f) != (size_t)p_length); } @@ -549,7 +408,7 @@ uint64_t FileAccessWindows::_get_modified_time(const String &p_file) { } String file = fix_path(p_file); - if (file.ends_with("/") && file != "/") { + if (file.ends_with("\\") && file != "\\") { file = file.substr(0, file.length() - 1); } @@ -582,14 +441,15 @@ bool FileAccessWindows::_get_hidden_attribute(const String &p_file) { Error FileAccessWindows::_set_hidden_attribute(const String &p_file, bool p_hidden) { String file = fix_path(p_file); + const Char16String &file_utf16 = file.utf16(); - DWORD attrib = GetFileAttributesW((LPCWSTR)file.utf16().get_data()); + DWORD attrib = GetFileAttributesW((LPCWSTR)file_utf16.get_data()); ERR_FAIL_COND_V_MSG(attrib == INVALID_FILE_ATTRIBUTES, FAILED, "Failed to get attributes for: " + p_file); BOOL ok; if (p_hidden) { - ok = SetFileAttributesW((LPCWSTR)file.utf16().get_data(), attrib | FILE_ATTRIBUTE_HIDDEN); + ok = SetFileAttributesW((LPCWSTR)file_utf16.get_data(), attrib | FILE_ATTRIBUTE_HIDDEN); } else { - ok = SetFileAttributesW((LPCWSTR)file.utf16().get_data(), attrib & ~FILE_ATTRIBUTE_HIDDEN); + ok = SetFileAttributesW((LPCWSTR)file_utf16.get_data(), attrib & ~FILE_ATTRIBUTE_HIDDEN); } ERR_FAIL_COND_V_MSG(!ok, FAILED, "Failed to set attributes for: " + p_file); @@ -606,14 +466,15 @@ bool FileAccessWindows::_get_read_only_attribute(const String &p_file) { Error FileAccessWindows::_set_read_only_attribute(const String &p_file, bool p_ro) { String file = fix_path(p_file); + const Char16String &file_utf16 = file.utf16(); - DWORD attrib = GetFileAttributesW((LPCWSTR)file.utf16().get_data()); + DWORD attrib = GetFileAttributesW((LPCWSTR)file_utf16.get_data()); ERR_FAIL_COND_V_MSG(attrib == INVALID_FILE_ATTRIBUTES, FAILED, "Failed to get attributes for: " + p_file); BOOL ok; if (p_ro) { - ok = SetFileAttributesW((LPCWSTR)file.utf16().get_data(), attrib | FILE_ATTRIBUTE_READONLY); + ok = SetFileAttributesW((LPCWSTR)file_utf16.get_data(), attrib | FILE_ATTRIBUTE_READONLY); } else { - ok = SetFileAttributesW((LPCWSTR)file.utf16().get_data(), attrib & ~FILE_ATTRIBUTE_READONLY); + ok = SetFileAttributesW((LPCWSTR)file_utf16.get_data(), attrib & ~FILE_ATTRIBUTE_READONLY); } ERR_FAIL_COND_V_MSG(!ok, FAILED, "Failed to set attributes for: " + p_file); diff --git a/drivers/windows/file_access_windows.h b/drivers/windows/file_access_windows.h index a25bbcfb3a..f458ff9c6c 100644 --- a/drivers/windows/file_access_windows.h +++ b/drivers/windows/file_access_windows.h @@ -69,20 +69,12 @@ public: virtual bool eof_reached() const override; ///< reading passed EOF - virtual uint8_t get_8() const override; ///< get a byte - virtual uint16_t get_16() const override; - virtual uint32_t get_32() const override; - virtual uint64_t get_64() const override; virtual uint64_t get_buffer(uint8_t *p_dst, uint64_t p_length) const override; virtual Error get_error() const override; ///< get last error virtual Error resize(int64_t p_length) override; virtual void flush() override; - virtual void store_8(uint8_t p_dest) override; ///< store a byte - virtual void store_16(uint16_t p_dest) override; - virtual void store_32(uint32_t p_dest) override; - virtual void store_64(uint64_t p_dest) override; virtual void store_buffer(const uint8_t *p_src, uint64_t p_length) override; ///< store an array of bytes virtual bool file_exists(const String &p_name) override; ///< return true if a file exists diff --git a/drivers/windows/file_access_windows_pipe.cpp b/drivers/windows/file_access_windows_pipe.cpp index 7902c8e1d8..0c953b14aa 100644 --- a/drivers/windows/file_access_windows_pipe.cpp +++ b/drivers/windows/file_access_windows_pipe.cpp @@ -96,22 +96,9 @@ String FileAccessWindowsPipe::get_path_absolute() const { return path_src; } -uint8_t FileAccessWindowsPipe::get_8() const { - ERR_FAIL_COND_V_MSG(fd[0] == 0, 0, "Pipe must be opened before use."); - - uint8_t b; - if (!ReadFile(fd[0], &b, 1, nullptr, nullptr)) { - last_error = ERR_FILE_CANT_READ; - b = '\0'; - } else { - last_error = OK; - } - return b; -} - uint64_t FileAccessWindowsPipe::get_buffer(uint8_t *p_dst, uint64_t p_length) const { - ERR_FAIL_COND_V(!p_dst && p_length > 0, -1); ERR_FAIL_COND_V_MSG(fd[0] == 0, -1, "Pipe must be opened before use."); + ERR_FAIL_COND_V(!p_dst && p_length > 0, -1); DWORD read = -1; if (!ReadFile(fd[0], p_dst, p_length, &read, nullptr) || read != p_length) { @@ -126,15 +113,6 @@ Error FileAccessWindowsPipe::get_error() const { return last_error; } -void FileAccessWindowsPipe::store_8(uint8_t p_src) { - ERR_FAIL_COND_MSG(fd[1] == 0, "Pipe must be opened before use."); - if (!WriteFile(fd[1], &p_src, 1, nullptr, nullptr)) { - last_error = ERR_FILE_CANT_WRITE; - } else { - last_error = OK; - } -} - void FileAccessWindowsPipe::store_buffer(const uint8_t *p_src, uint64_t p_length) { ERR_FAIL_COND_MSG(fd[1] == 0, "Pipe must be opened before use."); ERR_FAIL_COND(!p_src && p_length > 0); diff --git a/drivers/windows/file_access_windows_pipe.h b/drivers/windows/file_access_windows_pipe.h index b885ef78e6..4e9bd036ae 100644 --- a/drivers/windows/file_access_windows_pipe.h +++ b/drivers/windows/file_access_windows_pipe.h @@ -64,14 +64,12 @@ public: virtual bool eof_reached() const override { return false; } - virtual uint8_t get_8() const override; ///< get a byte virtual uint64_t get_buffer(uint8_t *p_dst, uint64_t p_length) const override; virtual Error get_error() const override; ///< get last error virtual Error resize(int64_t p_length) override { return ERR_UNAVAILABLE; } virtual void flush() override {} - virtual void store_8(uint8_t p_src) override; ///< store a byte virtual void store_buffer(const uint8_t *p_src, uint64_t p_length) override; ///< store an array of bytes virtual bool file_exists(const String &p_name) override { return false; } diff --git a/editor/action_map_editor.cpp b/editor/action_map_editor.cpp index 6b237366fd..16423fb111 100644 --- a/editor/action_map_editor.cpp +++ b/editor/action_map_editor.cpp @@ -584,7 +584,7 @@ ActionMapEditor::ActionMapEditor() { show_builtin_actions_checkbutton = memnew(CheckButton); show_builtin_actions_checkbutton->set_text(TTR("Show Built-in Actions")); - show_builtin_actions_checkbutton->connect("toggled", callable_mp(this, &ActionMapEditor::set_show_builtin_actions)); + show_builtin_actions_checkbutton->connect(SceneStringName(toggled), callable_mp(this, &ActionMapEditor::set_show_builtin_actions)); add_hbox->add_child(show_builtin_actions_checkbutton); show_builtin_actions = EditorSettings::get_singleton()->get_project_metadata("project_settings", "show_builtin_actions", false); diff --git a/editor/animation_bezier_editor.cpp b/editor/animation_bezier_editor.cpp index 5196857240..a2fba2c41e 100644 --- a/editor/animation_bezier_editor.cpp +++ b/editor/animation_bezier_editor.cpp @@ -1660,7 +1660,7 @@ void AnimationBezierTrackEdit::_menu_selected(int p_index) { switch (p_index) { case MENU_KEY_INSERT: { if (animation->get_track_count() > 0) { - if (editor->snap->is_pressed() && editor->step->get_value() != 0) { + if (editor->snap_keys->is_pressed() && editor->step->get_value() != 0) { time = editor->snap_time(time); } while (animation->track_find_key(selected_track, time, Animation::FIND_MODE_APPROX) != -1) { @@ -1736,7 +1736,7 @@ void AnimationBezierTrackEdit::duplicate_selected_keys(real_t p_ofs, bool p_ofs_ real_t insert_pos = p_ofs_valid ? p_ofs : timeline->get_play_position(); if (p_ofs_valid) { - if (editor->snap->is_pressed() && editor->step->get_value() != 0) { + if (editor->snap_keys->is_pressed() && editor->step->get_value() != 0) { insert_pos = editor->snap_time(insert_pos); } } @@ -1859,7 +1859,7 @@ void AnimationBezierTrackEdit::paste_keys(real_t p_ofs, bool p_ofs_valid) { float insert_pos = p_ofs_valid ? p_ofs : timeline->get_play_position(); if (p_ofs_valid) { - if (editor->snap->is_pressed() && editor->step->get_value() != 0) { + if (editor->snap_keys->is_pressed() && editor->step->get_value() != 0) { insert_pos = editor->snap_time(insert_pos); } } diff --git a/editor/animation_track_editor.cpp b/editor/animation_track_editor.cpp index 5706853b2a..95ba301282 100644 --- a/editor/animation_track_editor.cpp +++ b/editor/animation_track_editor.cpp @@ -41,6 +41,7 @@ #include "editor/gui/editor_spin_slider.h" #include "editor/gui/scene_tree_editor.h" #include "editor/inspector_dock.h" +#include "editor/multi_node_edit.h" #include "editor/plugins/animation_player_editor_plugin.h" #include "editor/themes/editor_scale.h" #include "scene/3d/mesh_instance_3d.h" @@ -120,12 +121,18 @@ bool AnimationTrackKeyEdit::_set(const StringName &p_name, const Variant &p_valu float val = p_value; float prev_val = animation->track_get_key_transition(track, key); setting = true; + EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); undo_redo->create_action(TTR("Animation Change Transition"), UndoRedo::MERGE_ENDS); undo_redo->add_do_method(animation.ptr(), "track_set_key_transition", track, key, val); undo_redo->add_undo_method(animation.ptr(), "track_set_key_transition", track, key, prev_val); undo_redo->add_do_method(this, "_update_obj", animation); undo_redo->add_undo_method(this, "_update_obj", animation); + AnimationPlayerEditor *ape = AnimationPlayerEditor::get_singleton(); + if (ape) { + undo_redo->add_do_method(ape, "_animation_update_key_frame"); + undo_redo->add_undo_method(ape, "_animation_update_key_frame"); + } undo_redo->commit_action(); setting = false; @@ -177,12 +184,18 @@ bool AnimationTrackKeyEdit::_set(const StringName &p_name, const Variant &p_valu } setting = true; + undo_redo->create_action(TTR("Animation Change Keyframe Value"), UndoRedo::MERGE_ENDS); Variant prev = animation->track_get_key_value(track, key); undo_redo->add_do_method(animation.ptr(), "track_set_key_value", track, key, value); undo_redo->add_undo_method(animation.ptr(), "track_set_key_value", track, key, prev); undo_redo->add_do_method(this, "_update_obj", animation); undo_redo->add_undo_method(this, "_update_obj", animation); + AnimationPlayerEditor *ape = AnimationPlayerEditor::get_singleton(); + if (ape) { + undo_redo->add_do_method(ape, "_animation_update_key_frame"); + undo_redo->add_undo_method(ape, "_animation_update_key_frame"); + } undo_redo->commit_action(); setting = false; @@ -1540,14 +1553,18 @@ void AnimationTimelineEdit::_notification(int p_what) { max_digit_width = MAX(digit_width, max_digit_width); } const int max_sc = int(Math::ceil(zoomw / scale)); - const int max_sc_width = String::num(max_sc).length() * max_digit_width; + const int max_sc_width = String::num(max_sc).length() * Math::ceil(max_digit_width); + + const int min_margin = MAX(text_secondary_margin, text_primary_margin); while (!step_found) { int min = max_sc_width; if (decimals > 0) { - min += period_width + max_digit_width * decimals; + min += Math::ceil(period_width + max_digit_width * decimals); } + min += (min_margin * 2); + static const int _multp[3] = { 1, 2, 5 }; for (int i = 0; i < 3; i++) { step = (_multp[i] * dec); @@ -1603,10 +1620,11 @@ void AnimationTimelineEdit::_notification(int p_what) { int sc = int(Math::floor(pos * SC_ADJ)); int prev_sc = int(Math::floor(prev * SC_ADJ)); - bool sub = (sc % SC_ADJ); if ((sc / step) != (prev_sc / step) || (prev_sc < 0 && sc >= 0)) { int scd = sc < 0 ? prev_sc : sc; + bool sub = (((scd - (scd % step)) % (dec * 10)) != 0); + int line_margin = sub ? v_line_secondary_margin : v_line_primary_margin; int line_width = sub ? v_line_secondary_width : v_line_primary_width; Color line_color = sub ? v_line_secondary_color : v_line_primary_color; @@ -3281,6 +3299,11 @@ void AnimationTrackEdit::_menu_selected(int p_index) { undo_redo->create_action(TTR("Change Animation Update Mode")); undo_redo->add_do_method(animation.ptr(), "value_track_set_update_mode", track, update_mode); undo_redo->add_undo_method(animation.ptr(), "value_track_set_update_mode", track, animation->value_track_get_update_mode(track)); + AnimationPlayerEditor *ape = AnimationPlayerEditor::get_singleton(); + if (ape) { + undo_redo->add_do_method(ape, "_animation_update_key_frame"); + undo_redo->add_undo_method(ape, "_animation_update_key_frame"); + } undo_redo->commit_action(); queue_redraw(); @@ -3295,6 +3318,11 @@ void AnimationTrackEdit::_menu_selected(int p_index) { undo_redo->create_action(TTR("Change Animation Interpolation Mode")); undo_redo->add_do_method(animation.ptr(), "track_set_interpolation_type", track, interp_mode); undo_redo->add_undo_method(animation.ptr(), "track_set_interpolation_type", track, animation->track_get_interpolation_type(track)); + AnimationPlayerEditor *ape = AnimationPlayerEditor::get_singleton(); + if (ape) { + undo_redo->add_do_method(ape, "_animation_update_key_frame"); + undo_redo->add_undo_method(ape, "_animation_update_key_frame"); + } undo_redo->commit_action(); queue_redraw(); } break; @@ -3305,6 +3333,11 @@ void AnimationTrackEdit::_menu_selected(int p_index) { undo_redo->create_action(TTR("Change Animation Loop Mode")); undo_redo->add_do_method(animation.ptr(), "track_set_interpolation_loop_wrap", track, loop_wrap); undo_redo->add_undo_method(animation.ptr(), "track_set_interpolation_loop_wrap", track, animation->track_get_interpolation_loop_wrap(track)); + AnimationPlayerEditor *ape = AnimationPlayerEditor::get_singleton(); + if (ape) { + undo_redo->add_do_method(ape, "_animation_update_key_frame"); + undo_redo->add_undo_method(ape, "_animation_update_key_frame"); + } undo_redo->commit_action(); queue_redraw(); @@ -3582,7 +3615,8 @@ void AnimationTrackEditor::set_animation(const Ref<Animation> &p_anim, bool p_re _update_step_spinbox(); step->set_block_signals(false); step->set_read_only(false); - snap->set_disabled(false); + snap_keys->set_disabled(false); + snap_timeline->set_disabled(false); snap_mode->set_disabled(false); auto_fit->set_disabled(false); auto_fit_bezier->set_disabled(false); @@ -3603,7 +3637,8 @@ void AnimationTrackEditor::set_animation(const Ref<Animation> &p_anim, bool p_re step->set_value(0); step->set_block_signals(false); step->set_read_only(true); - snap->set_disabled(true); + snap_keys->set_disabled(true); + snap_timeline->set_disabled(true); snap_mode->set_disabled(true); bezier_edit_icon->set_disabled(true); auto_fit->set_disabled(true); @@ -3661,7 +3696,7 @@ void AnimationTrackEditor::update_keying() { EditorSelectionHistory *editor_history = EditorNode::get_singleton()->get_editor_selection_history(); if (is_visible_in_tree() && animation.is_valid() && editor_history->get_path_size() > 0) { Object *obj = ObjectDB::get_instance(editor_history->get_path_object(0)); - keying_enabled = Object::cast_to<Node>(obj) != nullptr; + keying_enabled = Object::cast_to<Node>(obj) != nullptr || Object::cast_to<MultiNodeEdit>(obj) != nullptr; } if (keying_enabled == keying) { @@ -4078,19 +4113,20 @@ void AnimationTrackEditor::_insert_animation_key(NodePath p_path, const Variant _query_insert(id); } -void AnimationTrackEditor::insert_node_value_key(Node *p_node, const String &p_property, const Variant &p_value, bool p_only_if_exists) { +void AnimationTrackEditor::insert_node_value_key(Node *p_node, const String &p_property, bool p_only_if_exists, bool p_advance) { ERR_FAIL_NULL(root); // Let's build a node path. - Node *node = p_node; - String path = root->get_path_to(node, true); + String path = root->get_path_to(p_node, true); + + Variant value = p_node->get(p_property); - if (Object::cast_to<AnimationPlayer>(node) && p_property == "current_animation") { - if (node == AnimationPlayerEditor::get_singleton()->get_player()) { + if (Object::cast_to<AnimationPlayer>(p_node) && p_property == "current_animation") { + if (p_node == AnimationPlayerEditor::get_singleton()->get_player()) { EditorNode::get_singleton()->show_warning(TTR("AnimationPlayer can't animate itself, only other players.")); return; } - _insert_animation_key(path, p_value); + _insert_animation_key(path, value); return; } @@ -4118,26 +4154,26 @@ void AnimationTrackEditor::insert_node_value_key(Node *p_node, const String &p_p InsertData id; id.path = np; id.track_idx = i; - id.value = p_value; + id.value = value; id.type = Animation::TYPE_VALUE; // TRANSLATORS: This describes the target of new animation track, will be inserted into another string. id.query = vformat(TTR("property '%s'"), p_property); - id.advance = false; + id.advance = p_advance; // Dialog insert. _query_insert(id); inserted = true; } else if (animation->track_get_type(i) == Animation::TYPE_BEZIER) { - Variant value; + Variant actual_value; String track_path = animation->track_get_path(i); if (track_path == np) { - value = p_value; // All good. + actual_value = value; // All good. } else { int sep = track_path.rfind(":"); if (sep != -1) { String base_path = track_path.substr(0, sep); if (base_path == np) { String value_name = track_path.substr(sep + 1); - value = p_value.get(value_name); + actual_value = value.get(value_name); } else { continue; } @@ -4149,10 +4185,10 @@ void AnimationTrackEditor::insert_node_value_key(Node *p_node, const String &p_p InsertData id; id.path = animation->track_get_path(i); id.track_idx = i; - id.value = value; + id.value = actual_value; id.type = Animation::TYPE_BEZIER; id.query = vformat(TTR("property '%s'"), p_property); - id.advance = false; + id.advance = p_advance; // Dialog insert. _query_insert(id); inserted = true; @@ -4165,105 +4201,41 @@ void AnimationTrackEditor::insert_node_value_key(Node *p_node, const String &p_p InsertData id; id.path = np; id.track_idx = -1; - id.value = p_value; + id.value = value; id.type = Animation::TYPE_VALUE; id.query = vformat(TTR("property '%s'"), p_property); - id.advance = false; + id.advance = p_advance; // Dialog insert. _query_insert(id); } -void AnimationTrackEditor::insert_value_key(const String &p_property, const Variant &p_value, bool p_advance) { +void AnimationTrackEditor::insert_value_key(const String &p_property, bool p_advance) { EditorSelectionHistory *history = EditorNode::get_singleton()->get_editor_selection_history(); ERR_FAIL_NULL(root); ERR_FAIL_COND(history->get_path_size() == 0); Object *obj = ObjectDB::get_instance(history->get_path_object(0)); - ERR_FAIL_NULL(Object::cast_to<Node>(obj)); - // Let's build a node path. - Node *node = Object::cast_to<Node>(obj); - String path = root->get_path_to(node, true); - - if (Object::cast_to<AnimationPlayer>(node) && p_property == "current_animation") { - if (node == AnimationPlayerEditor::get_singleton()->get_player()) { - EditorNode::get_singleton()->show_warning(TTR("AnimationPlayer can't animate itself, only other players.")); - return; - } - _insert_animation_key(path, p_value); - return; - } - - for (int i = 1; i < history->get_path_size(); i++) { - String prop = history->get_path_property(i); - ERR_FAIL_COND(prop.is_empty()); - path += ":" + prop; - } - - path += ":" + p_property; - - NodePath np = path; - - // Locate track. - - bool inserted = false; - - make_insert_queue(); - for (int i = 0; i < animation->get_track_count(); i++) { - if (animation->track_get_type(i) == Animation::TYPE_VALUE) { - if (animation->track_get_path(i) != np) { - continue; - } + Ref<MultiNodeEdit> multi_node_edit(obj); + if (multi_node_edit.is_valid()) { + Node *edited_scene = EditorNode::get_singleton()->get_edited_scene(); + ERR_FAIL_NULL(edited_scene); - InsertData id; - id.path = np; - id.track_idx = i; - id.value = p_value; - id.type = Animation::TYPE_VALUE; - id.query = vformat(TTR("property '%s'"), p_property); - id.advance = p_advance; - // Dialog insert. - _query_insert(id); - inserted = true; - } else if (animation->track_get_type(i) == Animation::TYPE_BEZIER) { - Variant value; - if (animation->track_get_path(i) == np) { - value = p_value; // All good. - } else { - String tpath = animation->track_get_path(i); - int index = tpath.rfind(":"); - if (NodePath(tpath.substr(0, index + 1)) == np) { - String subindex = tpath.substr(index + 1, tpath.length() - index); - value = p_value.get(subindex); - } else { - continue; - } - } + make_insert_queue(); - InsertData id; - id.path = animation->track_get_path(i); - id.track_idx = i; - id.value = value; - id.type = Animation::TYPE_BEZIER; - id.query = vformat(TTR("property '%s'"), p_property); - id.advance = p_advance; - // Dialog insert. - _query_insert(id); - inserted = true; + for (int i = 0; i < multi_node_edit->get_node_count(); ++i) { + Node *node = edited_scene->get_node(multi_node_edit->get_node(i)); + insert_node_value_key(node, p_property, false, p_advance); } - } - commit_insert_queue(); - if (!inserted) { - InsertData id; - id.path = np; - id.track_idx = -1; - id.value = p_value; - id.type = Animation::TYPE_VALUE; - id.query = vformat(TTR("property '%s'"), p_property); - id.advance = p_advance; - // Dialog insert. - _query_insert(id); + commit_insert_queue(); + } else { + Node *node = Object::cast_to<Node>(obj); + ERR_FAIL_NULL(node); + + make_insert_queue(); + insert_node_value_key(node, p_property, false, p_advance); + commit_insert_queue(); } } @@ -4500,7 +4472,14 @@ AnimationTrackEditor::TrackIndices AnimationTrackEditor::_confirm_insert(InsertD } break; case Animation::TYPE_BEZIER: { - value = animation->make_default_bezier_key(p_id.value); + int existing = animation->track_find_key(p_id.track_idx, time, Animation::FIND_MODE_APPROX); + if (existing != -1) { + Array arr = animation->track_get_key_value(p_id.track_idx, existing); + arr[0] = p_id.value; + value = arr; + } else { + value = animation->make_default_bezier_key(p_id.value); + } bezier_edit_icon->set_disabled(false); } break; default: { @@ -4578,8 +4557,16 @@ bool AnimationTrackEditor::is_key_clipboard_active() const { return key_clipboard.keys.size(); } -bool AnimationTrackEditor::is_snap_enabled() const { - return snap->is_pressed() ^ Input::get_singleton()->is_key_pressed(Key::CMD_OR_CTRL); +bool AnimationTrackEditor::is_snap_timeline_enabled() const { + return snap_timeline->is_pressed() ^ Input::get_singleton()->is_key_pressed(Key::CMD_OR_CTRL); +} + +bool AnimationTrackEditor::is_snap_keys_enabled() const { + return snap_keys->is_pressed() ^ Input::get_singleton()->is_key_pressed(Key::CMD_OR_CTRL); +} + +bool AnimationTrackEditor::is_bezier_editor_active() const { + return bezier_edit->is_visible(); } bool AnimationTrackEditor::can_add_reset_key() const { @@ -4915,7 +4902,8 @@ void AnimationTrackEditor::_notification(int p_what) { case NOTIFICATION_THEME_CHANGED: { zoom_icon->set_texture(get_editor_theme_icon(SNAME("Zoom"))); bezier_edit_icon->set_icon(get_editor_theme_icon(SNAME("EditBezier"))); - snap->set_icon(get_editor_theme_icon(SNAME("Snap"))); + snap_timeline->set_icon(get_editor_theme_icon(SNAME("SnapTimeline"))); + snap_keys->set_icon(get_editor_theme_icon(SNAME("SnapKeys"))); view_group->set_icon(get_editor_theme_icon(view_group->is_pressed() ? SNAME("AnimationTrackList") : SNAME("AnimationTrackGroup"))); selected_filter->set_icon(get_editor_theme_icon(SNAME("AnimationFilter"))); imported_anim_warning->set_icon(get_editor_theme_icon(SNAME("NodeWarning"))); @@ -5236,7 +5224,7 @@ int AnimationTrackEditor::_get_track_selected() { void AnimationTrackEditor::_insert_key_from_track(float p_ofs, int p_track) { ERR_FAIL_INDEX(p_track, animation->get_track_count()); - if (snap->is_pressed() && step->get_value() != 0) { + if (snap_keys->is_pressed() && step->get_value() != 0) { p_ofs = snap_time(p_ofs); } while (animation->track_find_key(p_track, p_ofs, Animation::FIND_MODE_APPROX) != -1) { // Make sure insertion point is valid. @@ -5643,6 +5631,14 @@ void AnimationTrackEditor::_move_selection_commit() { moving_selection = false; undo_redo->add_do_method(this, "_redraw_tracks"); undo_redo->add_undo_method(this, "_redraw_tracks"); + + // Update key frame. + AnimationPlayerEditor *ape = AnimationPlayerEditor::get_singleton(); + if (ape) { + undo_redo->add_do_method(ape, "_animation_update_key_frame"); + undo_redo->add_undo_method(ape, "_animation_update_key_frame"); + } + undo_redo->commit_action(); } @@ -5851,7 +5847,7 @@ void AnimationTrackEditor::_anim_duplicate_keys(float p_ofs, bool p_ofs_valid, i float insert_pos = p_ofs_valid ? p_ofs : timeline->get_play_position(); if (p_ofs_valid) { - if (snap->is_pressed() && step->get_value() != 0) { + if (snap_keys->is_pressed() && step->get_value() != 0) { insert_pos = snap_time(insert_pos); } } @@ -5999,7 +5995,7 @@ void AnimationTrackEditor::_anim_paste_keys(float p_ofs, bool p_ofs_valid, int p float insert_pos = p_ofs_valid ? p_ofs : timeline->get_play_position(); if (p_ofs_valid) { - if (snap->is_pressed() && step->get_value() != 0) { + if (snap_keys->is_pressed() && step->get_value() != 0) { insert_pos = snap_time(insert_pos); } } @@ -6880,8 +6876,8 @@ void AnimationTrackEditor::_edit_menu_pressed(int p_option) { _redraw_tracks(); _update_key_edit(); EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); - undo_redo->clear_history(true, undo_redo->get_history_id_for_object(animation.ptr())); - undo_redo->clear_history(true, undo_redo->get_history_id_for_object(this)); + undo_redo->clear_history(undo_redo->get_history_id_for_object(animation.ptr())); + undo_redo->clear_history(undo_redo->get_history_id_for_object(this)); } break; case EDIT_CLEAN_UP_ANIMATION: { @@ -7023,8 +7019,8 @@ void AnimationTrackEditor::_cleanup_animation(Ref<Animation> p_animation) { } EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); - undo_redo->clear_history(true, undo_redo->get_history_id_for_object(animation.ptr())); - undo_redo->clear_history(true, undo_redo->get_history_id_for_object(this)); + undo_redo->clear_history(undo_redo->get_history_id_for_object(animation.ptr())); + undo_redo->clear_history(undo_redo->get_history_id_for_object(this)); _update_tracks(); } @@ -7072,12 +7068,15 @@ void AnimationTrackEditor::_update_snap_unit() { if (timeline->is_using_fps()) { snap_unit = 1.0 / step->get_value(); } else { - snap_unit = 1.0 / Math::round(1.0 / step->get_value()); // Follow the snap behavior of the timeline editor. + double integer; + double fraction = Math::modf(step->get_value(), &integer); + fraction = 1.0 / Math::round(1.0 / fraction); + snap_unit = integer + fraction; } } float AnimationTrackEditor::snap_time(float p_value, bool p_relative) { - if (is_snap_enabled()) { + if (is_snap_keys_enabled()) { if (Input::get_singleton()->is_key_pressed(Key::SHIFT)) { // Use more precise snapping when holding Shift. snap_unit *= 0.25; @@ -7331,13 +7330,21 @@ AnimationTrackEditor::AnimationTrackEditor() { bottom_hb->add_child(view_group); bottom_hb->add_child(memnew(VSeparator)); - snap = memnew(Button); - snap->set_flat(true); - snap->set_text(TTR("Snap:") + " "); - bottom_hb->add_child(snap); - snap->set_disabled(true); - snap->set_toggle_mode(true); - snap->set_pressed(true); + snap_timeline = memnew(Button); + snap_timeline->set_flat(true); + bottom_hb->add_child(snap_timeline); + snap_timeline->set_disabled(true); + snap_timeline->set_toggle_mode(true); + snap_timeline->set_pressed(false); + snap_timeline->set_tooltip_text(TTR("Apply snapping to timeline cursor.")); + + snap_keys = memnew(Button); + snap_keys->set_flat(true); + bottom_hb->add_child(snap_keys); + snap_keys->set_disabled(true); + snap_keys->set_toggle_mode(true); + snap_keys->set_pressed(true); + snap_keys->set_tooltip_text(TTR("Apply snapping to selected key(s).")); step = memnew(EditorSpinSlider); step->set_min(0); @@ -7706,6 +7713,8 @@ void AnimationTrackKeyEditEditor::_time_edit_exited() { undo_redo->add_do_method(ate, "_select_at_anim", animation, track, new_time); undo_redo->add_undo_method(ate, "_select_at_anim", animation, track, key_data_cache.time); } + undo_redo->add_do_method(ape, "_animation_update_key_frame"); + undo_redo->add_undo_method(ape, "_animation_update_key_frame"); } undo_redo->commit_action(); diff --git a/editor/animation_track_editor.h b/editor/animation_track_editor.h index 59ee6535ac..8a263d7d20 100644 --- a/editor/animation_track_editor.h +++ b/editor/animation_track_editor.h @@ -408,7 +408,8 @@ class AnimationTrackEditor : public VBoxContainer { HSlider *zoom = nullptr; EditorSpinSlider *step = nullptr; TextureRect *zoom_icon = nullptr; - Button *snap = nullptr; + Button *snap_keys = nullptr; + Button *snap_timeline = nullptr; Button *bezier_edit_icon = nullptr; OptionButton *snap_mode = nullptr; Button *auto_fit = nullptr; @@ -713,8 +714,8 @@ public: void cleanup(); void set_anim_pos(float p_pos); - void insert_node_value_key(Node *p_node, const String &p_property, const Variant &p_value, bool p_only_if_exists = false); - void insert_value_key(const String &p_property, const Variant &p_value, bool p_advance); + void insert_node_value_key(Node *p_node, const String &p_property, bool p_only_if_exists = false, bool p_advance = false); + void insert_value_key(const String &p_property, bool p_advance); void insert_transform_key(Node3D *p_node, const String &p_sub, const Animation::TrackType p_type, const Variant &p_value); bool has_track(Node3D *p_node, const String &p_sub, const Animation::TrackType p_type); void make_insert_queue(); @@ -728,7 +729,9 @@ public: bool is_selection_active() const; bool is_key_clipboard_active() const; bool is_moving_selection() const; - bool is_snap_enabled() const; + bool is_snap_timeline_enabled() const; + bool is_snap_keys_enabled() const; + bool is_bezier_editor_active() const; bool can_add_reset_key() const; float get_moving_selection_offset() const; float snap_time(float p_value, bool p_relative = false); diff --git a/editor/code_editor.cpp b/editor/code_editor.cpp index 8664c167b5..dd8aa523c4 100644 --- a/editor/code_editor.cpp +++ b/editor/code_editor.cpp @@ -755,13 +755,13 @@ FindReplaceBar::FindReplaceBar() { hbc_option_search->add_child(case_sensitive); case_sensitive->set_text(TTR("Match Case")); case_sensitive->set_focus_mode(FOCUS_NONE); - case_sensitive->connect("toggled", callable_mp(this, &FindReplaceBar::_search_options_changed)); + case_sensitive->connect(SceneStringName(toggled), callable_mp(this, &FindReplaceBar::_search_options_changed)); whole_words = memnew(CheckBox); hbc_option_search->add_child(whole_words); whole_words->set_text(TTR("Whole Words")); whole_words->set_focus_mode(FOCUS_NONE); - whole_words->connect("toggled", callable_mp(this, &FindReplaceBar::_search_options_changed)); + whole_words->connect(SceneStringName(toggled), callable_mp(this, &FindReplaceBar::_search_options_changed)); // Replace toolbar replace_text = memnew(LineEdit); @@ -786,7 +786,7 @@ FindReplaceBar::FindReplaceBar() { hbc_option_replace->add_child(selection_only); selection_only->set_text(TTR("Selection Only")); selection_only->set_focus_mode(FOCUS_NONE); - selection_only->connect("toggled", callable_mp(this, &FindReplaceBar::_search_options_changed)); + selection_only->connect(SceneStringName(toggled), callable_mp(this, &FindReplaceBar::_search_options_changed)); hide_button = memnew(TextureButton); add_child(hide_button); @@ -1299,23 +1299,29 @@ void CodeTextEditor::toggle_inline_comment(const String &delimiter) { text_editor->end_complex_operation(); } -void CodeTextEditor::goto_line(int p_line) { +void CodeTextEditor::goto_line(int p_line, int p_column) { text_editor->remove_secondary_carets(); text_editor->deselect(); - text_editor->unfold_line(p_line); - callable_mp((TextEdit *)text_editor, &TextEdit::set_caret_line).call_deferred(p_line, true, true, 0, 0); + text_editor->unfold_line(CLAMP(p_line, 0, text_editor->get_line_count() - 1)); + text_editor->set_caret_line(p_line, false); + text_editor->set_caret_column(p_column, false); + // Defer in case the CodeEdit was just created and needs to be resized. + callable_mp((TextEdit *)text_editor, &TextEdit::adjust_viewport_to_caret).call_deferred(0); } void CodeTextEditor::goto_line_selection(int p_line, int p_begin, int p_end) { text_editor->remove_secondary_carets(); - text_editor->unfold_line(p_line); - callable_mp((TextEdit *)text_editor, &TextEdit::set_caret_line).call_deferred(p_line, true, true, 0, 0); - callable_mp((TextEdit *)text_editor, &TextEdit::set_caret_column).call_deferred(p_begin, true, 0); + text_editor->unfold_line(CLAMP(p_line, 0, text_editor->get_line_count() - 1)); text_editor->select(p_line, p_begin, p_line, p_end); + callable_mp((TextEdit *)text_editor, &TextEdit::adjust_viewport_to_caret).call_deferred(0); } -void CodeTextEditor::goto_line_centered(int p_line) { - goto_line(p_line); +void CodeTextEditor::goto_line_centered(int p_line, int p_column) { + text_editor->remove_secondary_carets(); + text_editor->deselect(); + text_editor->unfold_line(CLAMP(p_line, 0, text_editor->get_line_count() - 1)); + text_editor->set_caret_line(p_line, false); + text_editor->set_caret_column(p_column, false); callable_mp((TextEdit *)text_editor, &TextEdit::center_viewport_to_caret).call_deferred(0); } @@ -1443,13 +1449,7 @@ void CodeTextEditor::goto_error() { corrected_column -= tab_count * (indent_size - 1); } - if (text_editor->get_line_count() != error_line) { - text_editor->unfold_line(error_line); - } - text_editor->remove_secondary_carets(); - text_editor->set_caret_line(error_line); - text_editor->set_caret_column(corrected_column); - text_editor->center_viewport_to_caret(); + goto_line_centered(error_line, corrected_column); } } @@ -1548,7 +1548,8 @@ void CodeTextEditor::_set_show_warnings_panel(bool p_show) { } void CodeTextEditor::_toggle_scripts_pressed() { - ScriptEditor::get_singleton()->toggle_scripts_panel(); + ERR_FAIL_NULL(toggle_scripts_list); + toggle_scripts_list->set_visible(!toggle_scripts_list->is_visible()); update_toggle_scripts_button(); } @@ -1723,16 +1724,18 @@ void CodeTextEditor::set_code_complete_func(CodeTextEditorCodeCompleteFunc p_cod code_complete_ud = p_ud; } +void CodeTextEditor::set_toggle_list_control(Control *p_control) { + toggle_scripts_list = p_control; +} + void CodeTextEditor::show_toggle_scripts_button() { toggle_scripts_button->show(); } void CodeTextEditor::update_toggle_scripts_button() { - if (is_layout_rtl()) { - toggle_scripts_button->set_icon(get_editor_theme_icon(ScriptEditor::get_singleton()->is_scripts_panel_toggled() ? SNAME("Forward") : SNAME("Back"))); - } else { - toggle_scripts_button->set_icon(get_editor_theme_icon(ScriptEditor::get_singleton()->is_scripts_panel_toggled() ? SNAME("Back") : SNAME("Forward"))); - } + ERR_FAIL_NULL(toggle_scripts_list); + bool forward = toggle_scripts_list->is_visible() == is_layout_rtl(); + toggle_scripts_button->set_icon(get_editor_theme_icon(forward ? SNAME("Forward") : SNAME("Back"))); toggle_scripts_button->set_tooltip_text(vformat("%s (%s)", TTR("Toggle Scripts Panel"), ED_GET_SHORTCUT("script_editor/toggle_scripts_panel")->get_as_text())); } diff --git a/editor/code_editor.h b/editor/code_editor.h index 28f6944b66..e56405a4b2 100644 --- a/editor/code_editor.h +++ b/editor/code_editor.h @@ -161,6 +161,7 @@ class CodeTextEditor : public VBoxContainer { HBoxContainer *status_bar = nullptr; Button *toggle_scripts_button = nullptr; + Control *toggle_scripts_list = nullptr; Button *error_button = nullptr; Button *warning_button = nullptr; @@ -246,9 +247,9 @@ public: /// by adding or removing comment delimiter void toggle_inline_comment(const String &delimiter); - void goto_line(int p_line); + void goto_line(int p_line, int p_column = 0); void goto_line_selection(int p_line, int p_begin, int p_end); - void goto_line_centered(int p_line); + void goto_line_centered(int p_line, int p_column = 0); void set_executing_line(int p_line); void clear_executing_line(); @@ -285,6 +286,7 @@ public: void validate_script(); + void set_toggle_list_control(Control *p_control); void show_toggle_scripts_button(); void update_toggle_scripts_button(); diff --git a/editor/debugger/editor_debugger_node.cpp b/editor/debugger/editor_debugger_node.cpp index d3bd18c0e8..b4265f9fc0 100644 --- a/editor/debugger/editor_debugger_node.cpp +++ b/editor/debugger/editor_debugger_node.cpp @@ -213,8 +213,8 @@ void EditorDebuggerNode::_bind_methods() { } void EditorDebuggerNode::register_undo_redo(UndoRedo *p_undo_redo) { - p_undo_redo->set_method_notify_callback(_method_changeds, this); - p_undo_redo->set_property_notify_callback(_property_changeds, this); + p_undo_redo->set_method_notify_callback(_methods_changed, this); + p_undo_redo->set_property_notify_callback(_properties_changed, this); } EditorDebuggerRemoteObject *EditorDebuggerNode::get_inspected_remote_object() { @@ -303,7 +303,7 @@ void EditorDebuggerNode::stop(bool p_force) { }); _break_state_changed(); breakpoints.clear(); - EditorUndoRedoManager::get_singleton()->clear_history(false, EditorUndoRedoManager::REMOTE_HISTORY); + EditorUndoRedoManager::get_singleton()->clear_history(EditorUndoRedoManager::REMOTE_HISTORY, false); set_process(false); } @@ -720,7 +720,7 @@ void EditorDebuggerNode::_breakpoints_cleared_in_tree(int p_debugger) { } // Remote inspector/edit. -void EditorDebuggerNode::_method_changeds(void *p_ud, Object *p_base, const StringName &p_name, const Variant **p_args, int p_argcount) { +void EditorDebuggerNode::_methods_changed(void *p_ud, Object *p_base, const StringName &p_name, const Variant **p_args, int p_argcount) { if (!singleton) { return; } @@ -729,7 +729,7 @@ void EditorDebuggerNode::_method_changeds(void *p_ud, Object *p_base, const Stri }); } -void EditorDebuggerNode::_property_changeds(void *p_ud, Object *p_base, const StringName &p_property, const Variant &p_value) { +void EditorDebuggerNode::_properties_changed(void *p_ud, Object *p_base, const StringName &p_property, const Variant &p_value) { if (!singleton) { return; } diff --git a/editor/debugger/editor_debugger_node.h b/editor/debugger/editor_debugger_node.h index aef1d84758..12e097f652 100644 --- a/editor/debugger/editor_debugger_node.h +++ b/editor/debugger/editor_debugger_node.h @@ -193,8 +193,8 @@ public: // Remote inspector/edit. void request_remote_tree(); - static void _method_changeds(void *p_ud, Object *p_base, const StringName &p_name, const Variant **p_args, int p_argcount); - static void _property_changeds(void *p_ud, Object *p_base, const StringName &p_property, const Variant &p_value); + static void _methods_changed(void *p_ud, Object *p_base, const StringName &p_name, const Variant **p_args, int p_argcount); + static void _properties_changed(void *p_ud, Object *p_base, const StringName &p_property, const Variant &p_value); // LiveDebug void set_live_debugging(bool p_enabled); diff --git a/editor/editor_asset_installer.cpp b/editor/editor_asset_installer.cpp index b415c72c15..1e44a9bdc9 100644 --- a/editor/editor_asset_installer.cpp +++ b/editor/editor_asset_installer.cpp @@ -685,7 +685,7 @@ EditorAssetInstaller::EditorAssetInstaller() { show_source_files_button->set_toggle_mode(true); show_source_files_button->set_tooltip_text(TTR("Open the list of the asset contents and select which files to install.")); remapping_tools->add_child(show_source_files_button); - show_source_files_button->connect("toggled", callable_mp(this, &EditorAssetInstaller::_toggle_source_tree).bind(false)); + show_source_files_button->connect(SceneStringName(toggled), callable_mp(this, &EditorAssetInstaller::_toggle_source_tree).bind(false)); Button *target_dir_button = memnew(Button); target_dir_button->set_text(TTR("Change Install Folder")); @@ -698,7 +698,7 @@ EditorAssetInstaller::EditorAssetInstaller() { skip_toplevel_check = memnew(CheckBox); skip_toplevel_check->set_text(TTR("Ignore asset root")); skip_toplevel_check->set_tooltip_text(TTR("Ignore the root directory when extracting files.")); - skip_toplevel_check->connect("toggled", callable_mp(this, &EditorAssetInstaller::_set_skip_toplevel)); + skip_toplevel_check->connect(SceneStringName(toggled), callable_mp(this, &EditorAssetInstaller::_set_skip_toplevel)); remapping_tools->add_child(skip_toplevel_check); remapping_tools->add_spacer(); diff --git a/editor/editor_audio_buses.cpp b/editor/editor_audio_buses.cpp index 3b337997e0..c076c99cd3 100644 --- a/editor/editor_audio_buses.cpp +++ b/editor/editor_audio_buses.cpp @@ -1262,7 +1262,7 @@ void EditorAudioBuses::_load_default_layout() { file->set_text(String(TTR("Layout:")) + " " + layout_path.get_file()); AudioServer::get_singleton()->set_bus_layout(state); _rebuild_buses(); - EditorUndoRedoManager::get_singleton()->clear_history(true, EditorUndoRedoManager::GLOBAL_HISTORY); + EditorUndoRedoManager::get_singleton()->clear_history(EditorUndoRedoManager::GLOBAL_HISTORY); callable_mp(this, &EditorAudioBuses::_select_layout).call_deferred(); } @@ -1278,7 +1278,7 @@ void EditorAudioBuses::_file_dialog_callback(const String &p_string) { file->set_text(String(TTR("Layout:")) + " " + p_string.get_file()); AudioServer::get_singleton()->set_bus_layout(state); _rebuild_buses(); - EditorUndoRedoManager::get_singleton()->clear_history(true, EditorUndoRedoManager::GLOBAL_HISTORY); + EditorUndoRedoManager::get_singleton()->clear_history(EditorUndoRedoManager::GLOBAL_HISTORY); callable_mp(this, &EditorAudioBuses::_select_layout).call_deferred(); } else if (file_dialog->get_file_mode() == EditorFileDialog::FILE_MODE_SAVE_FILE) { @@ -1298,7 +1298,7 @@ void EditorAudioBuses::_file_dialog_callback(const String &p_string) { edited_path = p_string; file->set_text(String(TTR("Layout:")) + " " + p_string.get_file()); _rebuild_buses(); - EditorUndoRedoManager::get_singleton()->clear_history(true, EditorUndoRedoManager::GLOBAL_HISTORY); + EditorUndoRedoManager::get_singleton()->clear_history(EditorUndoRedoManager::GLOBAL_HISTORY); callable_mp(this, &EditorAudioBuses::_select_layout).call_deferred(); } } @@ -1397,7 +1397,7 @@ void EditorAudioBuses::open_layout(const String &p_path) { file->set_text(p_path.get_file()); AudioServer::get_singleton()->set_bus_layout(state); _rebuild_buses(); - EditorUndoRedoManager::get_singleton()->clear_history(true, EditorUndoRedoManager::GLOBAL_HISTORY); + EditorUndoRedoManager::get_singleton()->clear_history(EditorUndoRedoManager::GLOBAL_HISTORY); callable_mp(this, &EditorAudioBuses::_select_layout).call_deferred(); } diff --git a/editor/editor_autoload_settings.cpp b/editor/editor_autoload_settings.cpp index 32b2133f23..fb007aee28 100644 --- a/editor/editor_autoload_settings.cpp +++ b/editor/editor_autoload_settings.cpp @@ -88,7 +88,7 @@ void EditorAutoloadSettings::_notification(int p_what) { } bool EditorAutoloadSettings::_autoload_name_is_valid(const String &p_name, String *r_error) { - if (!p_name.is_valid_identifier()) { + if (!p_name.is_valid_ascii_identifier()) { if (r_error) { *r_error = TTR("Invalid name.") + " "; if (p_name.size() > 0 && p_name.left(1).is_numeric()) { diff --git a/editor/editor_build_profile.cpp b/editor/editor_build_profile.cpp index f55fbe03d8..42726a8b12 100644 --- a/editor/editor_build_profile.cpp +++ b/editor/editor_build_profile.cpp @@ -73,7 +73,7 @@ const bool EditorBuildProfile::build_option_disabled_by_default[BUILD_OPTION_MAX false, // TEXT_SERVER_COMPLEX false, // DYNAMIC_FONTS false, // WOFF2_FONTS - false, // GRPAHITE_FONTS + false, // GRAPHITE_FONTS false, // MSDFGEN }; @@ -91,7 +91,7 @@ const bool EditorBuildProfile::build_option_disable_values[BUILD_OPTION_MAX] = { false, // TEXT_SERVER_COMPLEX false, // DYNAMIC_FONTS false, // WOFF2_FONTS - false, // GRPAHITE_FONTS + false, // GRAPHITE_FONTS false, // MSDFGEN }; @@ -108,7 +108,7 @@ const EditorBuildProfile::BuildOptionCategory EditorBuildProfile::build_option_c BUILD_OPTION_CATEGORY_TEXT_SERVER, // TEXT_SERVER_COMPLEX BUILD_OPTION_CATEGORY_TEXT_SERVER, // DYNAMIC_FONTS BUILD_OPTION_CATEGORY_TEXT_SERVER, // WOFF2_FONTS - BUILD_OPTION_CATEGORY_TEXT_SERVER, // GRPAHITE_FONTS + BUILD_OPTION_CATEGORY_TEXT_SERVER, // GRAPHITE_FONTS BUILD_OPTION_CATEGORY_TEXT_SERVER, // MSDFGEN }; @@ -345,7 +345,7 @@ void EditorBuildProfile::_bind_methods() { BIND_ENUM_CONSTANT(BUILD_OPTION_TEXT_SERVER_ADVANCED); BIND_ENUM_CONSTANT(BUILD_OPTION_DYNAMIC_FONTS); BIND_ENUM_CONSTANT(BUILD_OPTION_WOFF2_FONTS); - BIND_ENUM_CONSTANT(BUILD_OPTION_GRPAHITE_FONTS); + BIND_ENUM_CONSTANT(BUILD_OPTION_GRAPHITE_FONTS); BIND_ENUM_CONSTANT(BUILD_OPTION_MSDFGEN); BIND_ENUM_CONSTANT(BUILD_OPTION_MAX); diff --git a/editor/editor_build_profile.h b/editor/editor_build_profile.h index a947365c7f..f34ab040d4 100644 --- a/editor/editor_build_profile.h +++ b/editor/editor_build_profile.h @@ -57,7 +57,7 @@ public: BUILD_OPTION_TEXT_SERVER_ADVANCED, BUILD_OPTION_DYNAMIC_FONTS, BUILD_OPTION_WOFF2_FONTS, - BUILD_OPTION_GRPAHITE_FONTS, + BUILD_OPTION_GRAPHITE_FONTS, BUILD_OPTION_MSDFGEN, BUILD_OPTION_MAX, }; diff --git a/editor/editor_file_system.cpp b/editor/editor_file_system.cpp index f75e438582..3d3caf59eb 100644 --- a/editor/editor_file_system.cpp +++ b/editor/editor_file_system.cpp @@ -33,8 +33,6 @@ #include "core/config/project_settings.h" #include "core/extension/gdextension_manager.h" #include "core/io/file_access.h" -#include "core/io/resource_importer.h" -#include "core/io/resource_loader.h" #include "core/io/resource_saver.h" #include "core/object/worker_thread_pool.h" #include "core/os/os.h" @@ -95,25 +93,35 @@ String EditorFileSystemDirectory::get_file(int p_idx) const { } String EditorFileSystemDirectory::get_path() const { - String p; - const EditorFileSystemDirectory *d = this; - while (d->parent) { - p = d->name.path_join(p); - d = d->parent; + int parents = 0; + const EditorFileSystemDirectory *efd = this; + // Determine the level of nesting. + while (efd->parent) { + parents++; + efd = efd->parent; } - return "res://" + p; -} + if (parents == 0) { + return "res://"; + } -String EditorFileSystemDirectory::get_file_path(int p_idx) const { - String file = get_file(p_idx); - const EditorFileSystemDirectory *d = this; - while (d->parent) { - file = d->name.path_join(file); - d = d->parent; + // Using PackedStringArray, because the path is built in reverse order. + PackedStringArray path_bits; + // Allocate an array based on nesting. It will store path bits. + path_bits.resize(parents + 2); // Last String is empty, so paths end with /. + String *path_write = path_bits.ptrw(); + path_write[0] = "res:/"; + + efd = this; + for (int i = parents; i > 0; i--) { + path_write[i] = efd->name; + efd = efd->parent; } + return String("/").join(path_bits); +} - return "res://" + file; +String EditorFileSystemDirectory::get_file_path(int p_idx) const { + return get_path().path_join(get_file(p_idx)); } Vector<String> EditorFileSystemDirectory::get_file_deps(int p_idx) const { @@ -228,30 +236,47 @@ EditorFileSystem::ScannedDirectory::~ScannedDirectory() { } void EditorFileSystem::_first_scan_filesystem() { + EditorProgress ep = EditorProgress("first_scan_filesystem", TTR("Project initialization"), 5); Ref<DirAccess> d = DirAccess::create(DirAccess::ACCESS_RESOURCES); first_scan_root_dir = memnew(ScannedDirectory); first_scan_root_dir->full_path = "res://"; HashSet<String> existing_class_names; + HashSet<String> extensions; + ep.step(TTR("Scanning file structure..."), 0, true); nb_files_total = _scan_new_dir(first_scan_root_dir, d); // This loads the global class names from the scripts and ensures that even if the // global_script_class_cache.cfg was missing or invalid, the global class names are valid in ScriptServer. - _first_scan_process_scripts(first_scan_root_dir, existing_class_names); + // At the same time, to prevent looping multiple times in all files, it looks for extensions. + ep.step(TTR("Loading global class names..."), 1, true); + _first_scan_process_scripts(first_scan_root_dir, existing_class_names, extensions); // Removing invalid global class to prevent having invalid paths in ScriptServer. _remove_invalid_global_class_names(existing_class_names); + // Processing extensions to add new extensions or remove invalid ones. + // Important to do it in the first scan so custom types, new class names, custom importers, etc... + // from extensions are ready to go before plugins, autoloads and resources validation/importation. + // At this point, a restart of the editor should not be needed so we don't use the return value. + ep.step(TTR("Verifying GDExtensions..."), 2, true); + GDExtensionManager::get_singleton()->ensure_extensions_loaded(extensions); + // Now that all the global class names should be loaded, create autoloads and plugins. // This is done after loading the global class names because autoloads and plugins can use // global class names. + ep.step(TTR("Creating autoload scripts..."), 3, true); ProjectSettingsEditor::get_singleton()->init_autoloads(); + + ep.step(TTR("Initializing plugins..."), 4, true); EditorNode::get_singleton()->init_plugins(); + + ep.step(TTR("Starting file scan..."), 5, true); } -void EditorFileSystem::_first_scan_process_scripts(const ScannedDirectory *p_scan_dir, HashSet<String> &p_existing_class_names) { +void EditorFileSystem::_first_scan_process_scripts(const ScannedDirectory *p_scan_dir, HashSet<String> &p_existing_class_names, HashSet<String> &p_extensions) { for (ScannedDirectory *scan_sub_dir : p_scan_dir->subdirs) { - _first_scan_process_scripts(scan_sub_dir, p_existing_class_names); + _first_scan_process_scripts(scan_sub_dir, p_existing_class_names, p_extensions); } for (const String &scan_file : p_scan_dir->files) { @@ -267,6 +292,8 @@ void EditorFileSystem::_first_scan_process_scripts(const ScannedDirectory *p_sca if (!script_class_name.is_empty()) { p_existing_class_names.insert(script_class_name); } + } else if (type == SNAME("GDExtension")) { + p_extensions.insert(path); } } } @@ -650,6 +677,12 @@ bool EditorFileSystem::_update_scan_actions() { Vector<String> reimports; Vector<String> reloads; + EditorProgress *ep = nullptr; + if (scan_actions.size() > 1) { + ep = memnew(EditorProgress("_update_scan_actions", TTR("Scanning actions..."), scan_actions.size())); + } + + int step_count = 0; for (const ItemAction &ia : scan_actions) { switch (ia.action) { case ItemAction::ACTION_NONE: { @@ -781,8 +814,14 @@ bool EditorFileSystem::_update_scan_actions() { } break; } + + if (ep) { + ep->step(ia.file, step_count++, false); + } } + memdelete_notnull(ep); + if (_scan_extensions()) { //needs editor restart //extensions also may provide filetypes to be imported, so they must run before importing @@ -862,6 +901,7 @@ void EditorFileSystem::scan() { // Set first_scan to false before the signals so the function doing_first_scan can return false // in editor_node to start the export if needed. first_scan = false; + ResourceImporter::load_on_startup = nullptr; emit_signal(SNAME("filesystem_changed")); emit_signal(SNAME("sources_changed"), sources_changed.size() > 0); } else { @@ -872,8 +912,6 @@ void EditorFileSystem::scan() { scan_total = 0; s.priority = Thread::PRIORITY_LOW; thread.start(_thread_func, this, s); - //tree->hide(); - //progress->show(); } } @@ -1022,9 +1060,8 @@ void EditorFileSystem::_process_file_system(const ScannedDirectory *p_scan_dir, } } else { - fi->type = ResourceFormatImporter::get_singleton()->get_resource_type(path); - fi->uid = ResourceFormatImporter::get_singleton()->get_resource_uid(path); - fi->import_group_file = ResourceFormatImporter::get_singleton()->get_import_group_file(path); + // Using get_resource_import_info() to prevent calling 3 times ResourceFormatImporter::_get_path_and_type. + ResourceFormatImporter::get_singleton()->get_resource_import_info(path, fi->type, fi->uid, fi->import_group_file); fi->script_class_name = _get_global_script_class(fi->type, path, &fi->script_class_extends, &fi->script_class_icon_path); fi->modified_time = 0; fi->import_modified_time = 0; @@ -1475,6 +1512,7 @@ void EditorFileSystem::_notification(int p_what) { // Set first_scan to false before the signals so the function doing_first_scan can return false // in editor_node to start the export if needed. first_scan = false; + ResourceImporter::load_on_startup = nullptr; if (changed) { emit_signal(SNAME("filesystem_changed")); } @@ -1497,6 +1535,7 @@ void EditorFileSystem::_notification(int p_what) { // Set first_scan to false before the signals so the function doing_first_scan can return false // in editor_node to start the export if needed. first_scan = false; + ResourceImporter::load_on_startup = nullptr; emit_signal(SNAME("filesystem_changed")); emit_signal(SNAME("sources_changed"), sources_changed.size() > 0); } @@ -1828,14 +1867,26 @@ void EditorFileSystem::_update_script_classes() { return; } - update_script_mutex.lock(); + { + MutexLock update_script_lock(update_script_mutex); - for (const KeyValue<String, ScriptInfo> &E : update_script_paths) { - _register_global_class_script(E.key, E.key, E.value.type, E.value.script_class_name, E.value.script_class_extends, E.value.script_class_icon_path); - } + EditorProgress *ep = nullptr; + if (update_script_paths.size() > 1) { + ep = memnew(EditorProgress("update_scripts_classes", TTR("Registering global classes..."), update_script_paths.size())); + } + + int step_count = 0; + for (const KeyValue<String, ScriptInfo> &E : update_script_paths) { + _register_global_class_script(E.key, E.key, E.value.type, E.value.script_class_name, E.value.script_class_extends, E.value.script_class_icon_path); + if (ep) { + ep->step(E.value.script_class_name, step_count++, false); + } + } + + memdelete_notnull(ep); - update_script_paths.clear(); - update_script_mutex.unlock(); + update_script_paths.clear(); + } ScriptServer::save_global_classes(); EditorNode::get_editor_data().script_class_save_icon_paths(); @@ -1856,8 +1907,14 @@ void EditorFileSystem::_update_script_documentation() { return; } - update_script_mutex.lock(); + MutexLock update_script_lock(update_script_mutex); + EditorProgress *ep = nullptr; + if (update_script_paths_documentation.size() > 1) { + ep = memnew(EditorProgress("update_script_paths_documentation", TTR("Updating scripts documentation"), update_script_paths_documentation.size())); + } + + int step_count = 0; for (const String &path : update_script_paths_documentation) { int index = -1; EditorFileSystemDirectory *efd = find_file(path, &index); @@ -1880,10 +1937,15 @@ void EditorFileSystem::_update_script_documentation() { } } } + + if (ep) { + ep->step(efd->files[index]->file, step_count++, false); + } } + memdelete_notnull(ep); + update_script_paths_documentation.clear(); - update_script_mutex.unlock(); } void EditorFileSystem::_process_update_pending() { @@ -1895,7 +1957,7 @@ void EditorFileSystem::_process_update_pending() { } void EditorFileSystem::_queue_update_script_class(const String &p_path, const String &p_type, const String &p_script_class_name, const String &p_script_class_extends, const String &p_script_class_icon_path) { - update_script_mutex.lock(); + MutexLock update_script_lock(update_script_mutex); ScriptInfo si; si.type = p_type; @@ -1905,8 +1967,6 @@ void EditorFileSystem::_queue_update_script_class(const String &p_path, const St update_script_paths.insert(p_path, si); update_script_paths_documentation.insert(p_path); - - update_script_mutex.unlock(); } void EditorFileSystem::_update_scene_groups() { @@ -1916,35 +1976,36 @@ void EditorFileSystem::_update_scene_groups() { EditorProgress *ep = nullptr; if (update_scene_paths.size() > 20) { - ep = memnew(EditorProgress("update_scene_groups", TTR("Update Scene Groups"), update_scene_paths.size())); + ep = memnew(EditorProgress("update_scene_groups", TTR("Updating Scene Groups"), update_scene_paths.size())); } int step_count = 0; - update_scene_mutex.lock(); - for (const String &path : update_scene_paths) { - ProjectSettings::get_singleton()->remove_scene_groups_cache(path); + { + MutexLock update_scene_lock(update_scene_mutex); + for (const String &path : update_scene_paths) { + ProjectSettings::get_singleton()->remove_scene_groups_cache(path); - int index = -1; - EditorFileSystemDirectory *efd = find_file(path, &index); + int index = -1; + EditorFileSystemDirectory *efd = find_file(path, &index); - if (!efd || index < 0) { - // The file was removed. - continue; - } + if (!efd || index < 0) { + // The file was removed. + continue; + } - const HashSet<StringName> scene_groups = PackedScene::get_scene_groups(path); - if (!scene_groups.is_empty()) { - ProjectSettings::get_singleton()->add_scene_groups_cache(path, scene_groups); - } + const HashSet<StringName> scene_groups = PackedScene::get_scene_groups(path); + if (!scene_groups.is_empty()) { + ProjectSettings::get_singleton()->add_scene_groups_cache(path, scene_groups); + } - if (ep) { - ep->step(TTR("Updating Scene Groups..."), step_count++); + if (ep) { + ep->step(efd->files[index]->file, step_count++, false); + } } - } - memdelete_notnull(ep); - update_scene_paths.clear(); - update_scene_mutex.unlock(); + memdelete_notnull(ep); + update_scene_paths.clear(); + } ProjectSettings::get_singleton()->save_scene_groups_cache(); } @@ -1959,9 +2020,8 @@ void EditorFileSystem::_update_pending_scene_groups() { } void EditorFileSystem::_queue_update_scene_groups(const String &p_path) { - update_scene_mutex.lock(); + MutexLock update_scene_lock(update_scene_mutex); update_scene_paths.insert(p_path); - update_scene_mutex.unlock(); } void EditorFileSystem::_get_all_scenes(EditorFileSystemDirectory *p_dir, HashSet<String> &r_list) { @@ -2351,14 +2411,16 @@ Error EditorFileSystem::_reimport_group(const String &p_group_file, const Vector return err; } -Error EditorFileSystem::_reimport_file(const String &p_file, const HashMap<StringName, Variant> &p_custom_options, const String &p_custom_importer, Variant *p_generator_parameters) { +Error EditorFileSystem::_reimport_file(const String &p_file, const HashMap<StringName, Variant> &p_custom_options, const String &p_custom_importer, Variant *p_generator_parameters, bool p_update_file_system) { print_verbose(vformat("EditorFileSystem: Importing file: %s", p_file)); uint64_t start_time = OS::get_singleton()->get_ticks_msec(); EditorFileSystemDirectory *fs = nullptr; int cpos = -1; - bool found = _find_file(p_file, &fs, cpos); - ERR_FAIL_COND_V_MSG(!found, ERR_FILE_NOT_FOUND, "Can't find file '" + p_file + "'."); + if (p_update_file_system) { + bool found = _find_file(p_file, &fs, cpos); + ERR_FAIL_COND_V_MSG(!found, ERR_FILE_NOT_FOUND, "Can't find file '" + p_file + "'."); + } //try to obtain existing params @@ -2412,11 +2474,13 @@ Error EditorFileSystem::_reimport_file(const String &p_file, const HashMap<Strin if (importer_name == "keep" || importer_name == "skip") { //keep files, do nothing. - fs->files[cpos]->modified_time = FileAccess::get_modified_time(p_file); - fs->files[cpos]->import_modified_time = FileAccess::get_modified_time(p_file + ".import"); - fs->files[cpos]->deps.clear(); - fs->files[cpos]->type = ""; - fs->files[cpos]->import_valid = false; + if (p_update_file_system) { + fs->files[cpos]->modified_time = FileAccess::get_modified_time(p_file); + fs->files[cpos]->import_modified_time = FileAccess::get_modified_time(p_file + ".import"); + fs->files[cpos]->deps.clear(); + fs->files[cpos]->type = ""; + fs->files[cpos]->import_valid = false; + } EditorResourcePreview::get_singleton()->check_for_invalidation(p_file); return OK; } @@ -2576,16 +2640,18 @@ Error EditorFileSystem::_reimport_file(const String &p_file, const HashMap<Strin } } - // Update cpos, newly created files could've changed the index of the reimported p_file. - _find_file(p_file, &fs, cpos); + if (p_update_file_system) { + // Update cpos, newly created files could've changed the index of the reimported p_file. + _find_file(p_file, &fs, cpos); - // Update modified times, to avoid reimport. - fs->files[cpos]->modified_time = FileAccess::get_modified_time(p_file); - fs->files[cpos]->import_modified_time = FileAccess::get_modified_time(p_file + ".import"); - fs->files[cpos]->deps = _get_dependencies(p_file); - fs->files[cpos]->type = importer->get_resource_type(); - fs->files[cpos]->uid = uid; - fs->files[cpos]->import_valid = fs->files[cpos]->type == "TextFile" ? true : ResourceLoader::is_import_valid(p_file); + // Update modified times, to avoid reimport. + fs->files[cpos]->modified_time = FileAccess::get_modified_time(p_file); + fs->files[cpos]->import_modified_time = FileAccess::get_modified_time(p_file + ".import"); + fs->files[cpos]->deps = _get_dependencies(p_file); + fs->files[cpos]->type = importer->get_resource_type(); + fs->files[cpos]->uid = uid; + fs->files[cpos]->import_valid = fs->files[cpos]->type == "TextFile" ? true : ResourceLoader::is_import_valid(p_file); + } if (ResourceUID::get_singleton()->has_id(uid)) { ResourceUID::get_singleton()->set_id(uid, p_file); @@ -2654,13 +2720,25 @@ void EditorFileSystem::reimport_files(const Vector<String> &p_files) { Vector<String> reloads; - EditorProgress pr("reimport", TTR("(Re)Importing Assets"), p_files.size()); + EditorProgress *ep = memnew(EditorProgress("reimport", TTR("(Re)Importing Assets"), p_files.size())); + + // The method reimport_files runs on the main thread, and if VSync is enabled + // or Update Continuously is disabled, Main::Iteration takes longer each frame. + // Each EditorProgress::step can trigger a redraw, and when there are many files to import, + // this could lead to a slow import process, especially when the editor is unfocused. + // Temporarily disabling VSync and low_processor_usage_mode while reimporting fixes this. + const bool old_low_processor_usage_mode = OS::get_singleton()->is_in_low_processor_usage_mode(); + const DisplayServer::VSyncMode old_vsync_mode = DisplayServer::get_singleton()->window_get_vsync_mode(DisplayServer::MAIN_WINDOW_ID); + OS::get_singleton()->set_low_processor_usage_mode(false); + DisplayServer::get_singleton()->window_set_vsync_mode(DisplayServer::VSyncMode::VSYNC_DISABLED); Vector<ImportFile> reimport_files; HashSet<String> groups_to_reimport; for (int i = 0; i < p_files.size(); i++) { + ep->step(TTR("Preparing files to reimport..."), i, false); + String file = p_files[i]; ResourceUID::ID uid = ResourceUID::get_singleton()->text_to_id(file); @@ -2700,6 +2778,8 @@ void EditorFileSystem::reimport_files(const Vector<String> &p_files) { reimport_files.sort(); + ep->step(TTR("Executing pre-reimport operations..."), 0, true); + // Emit the resource_reimporting signal for the single file before the actual importation. emit_signal(SNAME("resources_reimporting"), reloads); @@ -2720,7 +2800,7 @@ void EditorFileSystem::reimport_files(const Vector<String> &p_files) { if (i + 1 == reimport_files.size() || reimport_files[i + 1].importer != reimport_files[from].importer || groups_to_reimport.has(reimport_files[i + 1].path)) { if (from - i == 0) { // Single file, do not use threads. - pr.step(reimport_files[i].path.get_file(), i); + ep->step(reimport_files[i].path.get_file(), i, false); _reimport_file(reimport_files[i].path); } else { Ref<ResourceImporter> importer = ResourceFormatImporter::get_singleton()->get_importer_by_name(reimport_files[from].importer); @@ -2742,7 +2822,7 @@ void EditorFileSystem::reimport_files(const Vector<String> &p_files) { do { if (current_index < tdata.max_index.get()) { current_index = tdata.max_index.get(); - pr.step(reimport_files[current_index].path.get_file(), current_index); + ep->step(reimport_files[current_index].path.get_file(), current_index, false); } OS::get_singleton()->delay_usec(1); } while (!WorkerThreadPool::get_singleton()->is_group_task_completed(group_task)); @@ -2756,7 +2836,7 @@ void EditorFileSystem::reimport_files(const Vector<String> &p_files) { } } else { - pr.step(reimport_files[i].path.get_file(), i); + ep->step(reimport_files[i].path.get_file(), i, false); _reimport_file(reimport_files[i].path); // We need to increment the counter, maybe the next file is multithreaded @@ -2773,7 +2853,7 @@ void EditorFileSystem::reimport_files(const Vector<String> &p_files) { HashMap<String, Vector<String>> group_files; _find_group_files(filesystem, group_files, groups_to_reimport); for (const KeyValue<String, Vector<String>> &E : group_files) { - pr.step(E.key.get_file(), from++); + ep->step(E.key.get_file(), from++, false); Error err = _reimport_group(E.key, E.value); reloads.push_back(E.key); reloads.append_array(E.value); @@ -2782,17 +2862,28 @@ void EditorFileSystem::reimport_files(const Vector<String> &p_files) { } } } + ep->step(TTR("Finalizing Asset Import..."), p_files.size()); ResourceUID::get_singleton()->update_cache(); // After reimporting, update the cache. - _save_filesystem_cache(); + + memdelete_notnull(ep); + _process_update_pending(); + + // Revert to previous values to restore editor settings for VSync and Update Continuously. + OS::get_singleton()->set_low_processor_usage_mode(old_low_processor_usage_mode); + DisplayServer::get_singleton()->window_set_vsync_mode(old_vsync_mode); + importing = false; + + ep = memnew(EditorProgress("reimport", TTR("(Re)Importing Assets"), p_files.size())); + ep->step(TTR("Executing post-reimport operations..."), 0, true); if (!is_scanning()) { emit_signal(SNAME("filesystem_changed")); } - emit_signal(SNAME("resources_reimported"), reloads); + memdelete_notnull(ep); } Error EditorFileSystem::reimport_append(const String &p_file, const HashMap<StringName, Variant> &p_custom_options, const String &p_custom_importer, Variant p_generator_parameters) { @@ -2820,6 +2911,33 @@ Error EditorFileSystem::_resource_import(const String &p_path) { return OK; } +Ref<Resource> EditorFileSystem::_load_resource_on_startup(ResourceFormatImporter *p_importer, const String &p_path, Error *r_error, bool p_use_sub_threads, float *r_progress, ResourceFormatLoader::CacheMode p_cache_mode) { + ERR_FAIL_NULL_V(p_importer, Ref<Resource>()); + + if (!FileAccess::exists(p_path)) { + ERR_FAIL_V_MSG(Ref<Resource>(), vformat("Failed loading resource: %s. The file doesn't seem to exist.", p_path)); + } + + Ref<Resource> res; + bool can_retry = true; + bool retry = true; + while (retry) { + retry = false; + + res = p_importer->load_internal(p_path, r_error, p_use_sub_threads, r_progress, p_cache_mode, can_retry); + + if (res.is_null() && can_retry) { + can_retry = false; + Error err = singleton->_reimport_file(p_path, HashMap<StringName, Variant>(), "", nullptr, false); + if (err == OK) { + retry = true; + } + } + } + + return res; +} + bool EditorFileSystem::_should_skip_directory(const String &p_path) { String project_data_path = ProjectSettings::get_singleton()->get_project_data_path(); if (p_path == project_data_path || p_path.begins_with(project_data_path + "/")) { @@ -2941,54 +3059,7 @@ bool EditorFileSystem::_scan_extensions() { _scan_extensions_dir(d, extensions); - //verify against loaded extensions - - Vector<String> extensions_added; - Vector<String> extensions_removed; - - for (const String &E : extensions) { - if (!GDExtensionManager::get_singleton()->is_extension_loaded(E)) { - extensions_added.push_back(E); - } - } - - Vector<String> loaded_extensions = GDExtensionManager::get_singleton()->get_loaded_extensions(); - for (int i = 0; i < loaded_extensions.size(); i++) { - if (!extensions.has(loaded_extensions[i])) { - extensions_removed.push_back(loaded_extensions[i]); - } - } - - String extension_list_config_file = GDExtension::get_extension_list_config_file(); - if (extensions.size()) { - if (extensions_added.size() || extensions_removed.size()) { //extensions were added or removed - Ref<FileAccess> f = FileAccess::open(extension_list_config_file, FileAccess::WRITE); - for (const String &E : extensions) { - f->store_line(E); - } - } - } else { - if (loaded_extensions.size() || FileAccess::exists(extension_list_config_file)) { //extensions were removed - Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_RESOURCES); - da->remove(extension_list_config_file); - } - } - - bool needs_restart = false; - for (int i = 0; i < extensions_added.size(); i++) { - GDExtensionManager::LoadStatus st = GDExtensionManager::get_singleton()->load_extension(extensions_added[i]); - if (st == GDExtensionManager::LOAD_STATUS_NEEDS_RESTART) { - needs_restart = true; - } - } - for (int i = 0; i < extensions_removed.size(); i++) { - GDExtensionManager::LoadStatus st = GDExtensionManager::get_singleton()->unload_extension(extensions_removed[i]); - if (st == GDExtensionManager::LOAD_STATUS_NEEDS_RESTART) { - needs_restart = true; - } - } - - return needs_restart; + return GDExtensionManager::get_singleton()->ensure_extensions_loaded(extensions); } void EditorFileSystem::_bind_methods() { @@ -3064,6 +3135,10 @@ EditorFileSystem::EditorFileSystem() { scan_total = 0; ResourceSaver::set_get_resource_id_for_path(_resource_saver_get_resource_id_for_path); + + // Set the callback method that the ResourceFormatImporter will use + // if resources are loaded during the first scan. + ResourceImporter::load_on_startup = _load_resource_on_startup; } EditorFileSystem::~EditorFileSystem() { diff --git a/editor/editor_file_system.h b/editor/editor_file_system.h index 1bc24416eb..2ceb3fe4a5 100644 --- a/editor/editor_file_system.h +++ b/editor/editor_file_system.h @@ -32,6 +32,8 @@ #define EDITOR_FILE_SYSTEM_H #include "core/io/dir_access.h" +#include "core/io/resource_importer.h" +#include "core/io/resource_loader.h" #include "core/os/thread.h" #include "core/os/thread_safe.h" #include "core/templates/hash_set.h" @@ -187,7 +189,7 @@ class EditorFileSystem : public Node { void _scan_filesystem(); void _first_scan_filesystem(); - void _first_scan_process_scripts(const ScannedDirectory *p_scan_dir, HashSet<String> &p_existing_class_names); + void _first_scan_process_scripts(const ScannedDirectory *p_scan_dir, HashSet<String> &p_existing_class_names, HashSet<String> &p_extensions); HashSet<String> late_update_files; @@ -252,7 +254,7 @@ class EditorFileSystem : public Node { void _update_extensions(); - Error _reimport_file(const String &p_file, const HashMap<StringName, Variant> &p_custom_options = HashMap<StringName, Variant>(), const String &p_custom_importer = String(), Variant *generator_parameters = nullptr); + Error _reimport_file(const String &p_file, const HashMap<StringName, Variant> &p_custom_options = HashMap<StringName, Variant>(), const String &p_custom_importer = String(), Variant *generator_parameters = nullptr, bool p_update_file_system = true); Error _reimport_group(const String &p_group_file, const Vector<String> &p_files); bool _test_for_reimport(const String &p_path, bool p_only_imported_files); @@ -296,6 +298,7 @@ class EditorFileSystem : public Node { String _get_global_script_class(const String &p_type, const String &p_path, String *r_extends, String *r_icon_path) const; static Error _resource_import(const String &p_path); + static Ref<Resource> _load_resource_on_startup(ResourceFormatImporter *p_importer, const String &p_path, Error *r_error, bool p_use_sub_threads, float *r_progress, ResourceFormatLoader::CacheMode p_cache_mode); bool using_fat32_or_exfat; // Workaround for projects in FAT32 or exFAT filesystem (pendrives, most of the time) diff --git a/editor/editor_help.cpp b/editor/editor_help.cpp index 683e4e5cda..9b0c05d910 100644 --- a/editor/editor_help.cpp +++ b/editor/editor_help.cpp @@ -288,7 +288,7 @@ void EditorHelp::_class_desc_select(const String &p_select) { // Case order is important here to correctly handle edge cases like Variant.Type in @GlobalScope. if (table->has(link)) { // Found in the current page. - if (class_desc->is_ready()) { + if (class_desc->is_finished()) { emit_signal(SNAME("request_save_history")); class_desc->scroll_to_paragraph((*table)[link]); } else { @@ -2338,7 +2338,7 @@ void EditorHelp::_help_callback(const String &p_topic) { } } - if (class_desc->is_ready()) { + if (class_desc->is_finished()) { // call_deferred() is not enough. if (class_desc->is_connected(SceneStringName(draw), callable_mp(class_desc, &RichTextLabel::scroll_to_paragraph))) { class_desc->disconnect(SceneStringName(draw), callable_mp(class_desc, &RichTextLabel::scroll_to_paragraph)); @@ -3040,7 +3040,7 @@ Vector<Pair<String, int>> EditorHelp::get_sections() { void EditorHelp::scroll_to_section(int p_section_index) { _wait_for_thread(); int line = section_line[p_section_index].second; - if (class_desc->is_ready()) { + if (class_desc->is_finished()) { class_desc->scroll_to_paragraph(line); } else { scroll_to = line; diff --git a/editor/editor_help_search.cpp b/editor/editor_help_search.cpp index ff5bc6ba87..eb97337b37 100644 --- a/editor/editor_help_search.cpp +++ b/editor/editor_help_search.cpp @@ -134,18 +134,38 @@ void EditorHelpSearch::_native_action_cb(const String &p_item_string) { } void EditorHelpSearch::_update_results() { - String term = search_box->get_text(); + const String term = search_box->get_text().strip_edges(); int search_flags = filter_combo->get_selected_id(); - if (case_sensitive_button->is_pressed()) { - search_flags |= SEARCH_CASE_SENSITIVE; - } - if (hierarchy_button->is_pressed()) { - search_flags |= SEARCH_SHOW_HIERARCHY; - } - search = Ref<Runner>(memnew(Runner(results_tree, results_tree, &tree_cache, term, search_flags))); - set_process(true); + // Process separately if term is not short, or is "@" for annotations. + if (term.length() > 1 || term == "@") { + case_sensitive_button->set_disabled(false); + hierarchy_button->set_disabled(false); + + if (case_sensitive_button->is_pressed()) { + search_flags |= SEARCH_CASE_SENSITIVE; + } + if (hierarchy_button->is_pressed()) { + search_flags |= SEARCH_SHOW_HIERARCHY; + } + + search = Ref<Runner>(memnew(Runner(results_tree, results_tree, &tree_cache, term, search_flags))); + + // Clear old search flags to force rebuild on short term. + old_search_flags = 0; + set_process(true); + } else { + // Disable hierarchy and case sensitive options, not used for short searches. + case_sensitive_button->set_disabled(true); + hierarchy_button->set_disabled(true); + + // Always show hierarchy for short searches. + search = Ref<Runner>(memnew(Runner(results_tree, results_tree, &tree_cache, term, search_flags | SEARCH_SHOW_HIERARCHY))); + + old_search_flags = search_flags; + set_process(true); + } } void EditorHelpSearch::_search_box_gui_input(const Ref<InputEvent> &p_event) { @@ -205,6 +225,8 @@ void EditorHelpSearch::_notification(int p_what) { case NOTIFICATION_VISIBILITY_CHANGED: { if (!is_visible()) { tree_cache.clear(); + results_tree->get_vscroll_bar()->set_value(0); + search = Ref<Runner>(); callable_mp(results_tree, &Tree::clear).call_deferred(); // Wait for the Tree's mouse event propagation. get_ok_button()->set_disabled(true); EditorSettings::get_singleton()->set_project_metadata("dialog_bounds", "search_help", Rect2(get_position(), get_size())); @@ -278,6 +300,7 @@ void EditorHelpSearch::popup_dialog(const String &p_term) { popup_centered_ratio(0.5F); } + old_search_flags = 0; if (p_term.is_empty()) { search_box->clear(); } else { @@ -401,6 +424,237 @@ bool EditorHelpSearch::Runner::_is_class_disabled_by_feature_profile(const Strin return false; } +bool EditorHelpSearch::Runner::_fill() { + bool phase_done = false; + switch (phase) { + case PHASE_MATCH_CLASSES_INIT: + phase_done = _phase_fill_classes_init(); + break; + case PHASE_MATCH_CLASSES: + phase_done = _phase_fill_classes(); + break; + case PHASE_CLASS_ITEMS_INIT: + case PHASE_CLASS_ITEMS: + phase_done = true; + break; + case PHASE_MEMBER_ITEMS_INIT: + phase_done = _phase_fill_member_items_init(); + break; + case PHASE_MEMBER_ITEMS: + phase_done = _phase_fill_member_items(); + break; + case PHASE_SELECT_MATCH: + phase_done = _phase_select_match(); + break; + case PHASE_MAX: + return true; + default: + WARN_PRINT("Invalid or unhandled phase in EditorHelpSearch::Runner, aborting search."); + return true; + } + + if (phase_done) { + phase++; + } + return false; +} + +bool EditorHelpSearch::Runner::_phase_fill_classes_init() { + // Initialize fill. + iterator_stack.clear(); + matched_classes.clear(); + matched_item = nullptr; + match_highest_score = 0; + + // Initialize stack of iterators to fill, in reverse. + iterator_stack.push_back(EditorHelp::get_doc_data()->inheriting[""].back()); + + return true; +} + +bool EditorHelpSearch::Runner::_phase_fill_classes() { + if (iterator_stack.is_empty()) { + return true; + } + + if (iterator_stack[iterator_stack.size() - 1]) { + DocData::ClassDoc *class_doc = EditorHelp::get_doc_data()->class_list.getptr(iterator_stack[iterator_stack.size() - 1]->get()); + + // Decrement stack. + iterator_stack[iterator_stack.size() - 1] = iterator_stack[iterator_stack.size() - 1]->prev(); + + // Drop last element of stack if empty. + if (!iterator_stack[iterator_stack.size() - 1]) { + iterator_stack.resize(iterator_stack.size() - 1); + } + + if (!class_doc || class_doc->name.is_empty()) { + return false; + } + + // If class matches the flags, add it to the matched stack. + const bool class_matched = + (search_flags & SEARCH_CLASSES) || + ((search_flags & SEARCH_CONSTRUCTORS) && !class_doc->constructors.is_empty()) || + ((search_flags & SEARCH_METHODS) && !class_doc->methods.is_empty()) || + ((search_flags & SEARCH_OPERATORS) && !class_doc->operators.is_empty()) || + ((search_flags & SEARCH_SIGNALS) && !class_doc->signals.is_empty()) || + ((search_flags & SEARCH_CONSTANTS) && !class_doc->constants.is_empty()) || + ((search_flags & SEARCH_PROPERTIES) && !class_doc->properties.is_empty()) || + ((search_flags & SEARCH_THEME_ITEMS) && !class_doc->theme_properties.is_empty()) || + ((search_flags & SEARCH_ANNOTATIONS) && !class_doc->annotations.is_empty()); + + if (class_matched) { + if (term.is_empty() || class_doc->name.containsn(term)) { + matched_classes.push_back(Pair<DocData::ClassDoc *, String>(class_doc, String())); + } else if (String keyword = _match_keywords(term, class_doc->keywords); !keyword.is_empty()) { + matched_classes.push_back(Pair<DocData::ClassDoc *, String>(class_doc, keyword)); + } + } + + // Add inheriting classes, in reverse. + if (class_doc && EditorHelp::get_doc_data()->inheriting.has(class_doc->name)) { + iterator_stack.push_back(EditorHelp::get_doc_data()->inheriting[class_doc->name].back()); + } + + return false; + } + + // Drop last element of stack if empty. + if (!iterator_stack[iterator_stack.size() - 1]) { + iterator_stack.resize(iterator_stack.size() - 1); + } + + return iterator_stack.is_empty(); +} + +bool EditorHelpSearch::Runner::_phase_fill_member_items_init() { + // Prepare tree. + class_items.clear(); + _populate_cache(); + + return true; +} + +TreeItem *EditorHelpSearch::Runner::_create_category_item(TreeItem *p_parent, const String &p_class, const StringName &p_icon, const String &p_metatype, const String &p_text) { + const String item_meta = "class_" + p_metatype + ":" + p_class; + + TreeItem *item = nullptr; + if (_find_or_create_item(p_parent, item_meta, item)) { + item->set_icon(0, ui_service->get_editor_theme_icon(p_icon)); + item->set_text(0, p_text); + item->set_metadata(0, item_meta); + } + item->set_collapsed(true); + + return item; +} + +bool EditorHelpSearch::Runner::_phase_fill_member_items() { + if (matched_classes.is_empty()) { + return true; + } + + // Pop working item from stack. + Pair<DocData::ClassDoc *, String> match = matched_classes[matched_classes.size() - 1]; + DocData::ClassDoc *class_doc = match.first; + const String &keyword = match.second; + matched_classes.resize(matched_classes.size() - 1); + + if (class_doc) { + TreeItem *item = _create_class_hierarchy(class_doc, keyword, !(search_flags & SEARCH_CLASSES)); + + // If the class has no inheriting classes, fold its item. + item->set_collapsed(!item->get_first_child()); + + if (search_flags & SEARCH_CLASSES) { + item->clear_custom_color(0); + item->clear_custom_color(1); + } else { + item->set_custom_color(0, disabled_color); + item->set_custom_color(1, disabled_color); + } + + // Create common header if required. + const bool search_all = (search_flags & SEARCH_ALL) == SEARCH_ALL; + + if ((search_flags & SEARCH_CONSTRUCTORS) && !class_doc->constructors.is_empty()) { + TreeItem *parent_item = item; + if (search_all) { + parent_item = _create_category_item(parent_item, class_doc->name, SNAME("MemberConstructor"), TTRC("Constructors"), "constructors"); + } + for (const DocData::MethodDoc &constructor_doc : class_doc->constructors) { + _create_constructor_item(parent_item, class_doc, &constructor_doc); + } + } + if ((search_flags & SEARCH_METHODS) && !class_doc->methods.is_empty()) { + TreeItem *parent_item = item; + if (search_all) { + parent_item = _create_category_item(parent_item, class_doc->name, SNAME("MemberMethod"), TTRC("Methods"), "methods"); + } + for (const DocData::MethodDoc &method_doc : class_doc->methods) { + _create_method_item(parent_item, class_doc, &method_doc); + } + } + if ((search_flags & SEARCH_OPERATORS) && !class_doc->operators.is_empty()) { + TreeItem *parent_item = item; + if (search_all) { + parent_item = _create_category_item(parent_item, class_doc->name, SNAME("MemberOperator"), TTRC("Operators"), "operators"); + } + for (const DocData::MethodDoc &operator_doc : class_doc->operators) { + _create_operator_item(parent_item, class_doc, &operator_doc); + } + } + if ((search_flags & SEARCH_SIGNALS) && !class_doc->signals.is_empty()) { + TreeItem *parent_item = item; + if (search_all) { + parent_item = _create_category_item(parent_item, class_doc->name, SNAME("MemberSignal"), TTRC("Signals"), "signals"); + } + for (const DocData::MethodDoc &signal_doc : class_doc->signals) { + _create_signal_item(parent_item, class_doc, &signal_doc); + } + } + if ((search_flags & SEARCH_CONSTANTS) && !class_doc->constants.is_empty()) { + TreeItem *parent_item = item; + if (search_all) { + parent_item = _create_category_item(parent_item, class_doc->name, SNAME("MemberConstant"), TTRC("Constants"), "constants"); + } + for (const DocData::ConstantDoc &constant_doc : class_doc->constants) { + _create_constant_item(parent_item, class_doc, &constant_doc); + } + } + if ((search_flags & SEARCH_PROPERTIES) && !class_doc->properties.is_empty()) { + TreeItem *parent_item = item; + if (search_all) { + parent_item = _create_category_item(parent_item, class_doc->name, SNAME("MemberProperty"), TTRC("Prtoperties"), "propertiess"); + } + for (const DocData::PropertyDoc &property_doc : class_doc->properties) { + _create_property_item(parent_item, class_doc, &property_doc); + } + } + if ((search_flags & SEARCH_THEME_ITEMS) && !class_doc->theme_properties.is_empty()) { + TreeItem *parent_item = item; + if (search_all) { + parent_item = _create_category_item(parent_item, class_doc->name, SNAME("MemberTheme"), TTRC("Theme Properties"), "theme_items"); + } + for (const DocData::ThemeItemDoc &theme_property_doc : class_doc->theme_properties) { + _create_theme_property_item(parent_item, class_doc, &theme_property_doc); + } + } + if ((search_flags & SEARCH_ANNOTATIONS) && !class_doc->annotations.is_empty()) { + TreeItem *parent_item = item; + if (search_all) { + parent_item = _create_category_item(parent_item, class_doc->name, SNAME("MemberAnnotation"), TTRC("Annotations"), "annotations"); + } + for (const DocData::MethodDoc &annotation_doc : class_doc->annotations) { + _create_annotation_item(parent_item, class_doc, &annotation_doc); + } + } + } + + return matched_classes.is_empty(); +} + bool EditorHelpSearch::Runner::_slice() { bool phase_done = false; switch (phase) { @@ -430,7 +684,7 @@ bool EditorHelpSearch::Runner::_slice() { default: WARN_PRINT("Invalid or unhandled phase in EditorHelpSearch::Runner, aborting search."); return true; - }; + } if (phase_done) { phase++; @@ -450,9 +704,11 @@ bool EditorHelpSearch::Runner::_phase_match_classes_init() { matched_item = nullptr; match_highest_score = 0; - terms = term.split_spaces(); - if (terms.is_empty()) { - terms.append(term); + if (!term.is_empty()) { + terms = term.split_spaces(); + if (terms.is_empty()) { + terms.append(term); + } } return true; @@ -480,78 +736,71 @@ bool EditorHelpSearch::Runner::_phase_match_classes() { // Match class name. if (search_flags & SEARCH_CLASSES) { - // If the search term is empty, add any classes which are not script docs or which don't start with - // a double-quotation. This will ensure that only C++ classes and explicitly named classes will - // be added. - match.name = (term.is_empty() && (!class_doc->is_script_doc || class_doc->name[0] != '\"')) || _match_string(term, class_doc->name); + match.name = _match_string(term, class_doc->name); match.keyword = _match_keywords(term, class_doc->keywords); } - // Match members only if the term is long enough, to avoid slow performance from building a large tree. - // Make an exception for annotations, since there are not that many of them. - if (term.length() > 1 || term == "@") { - if (search_flags & SEARCH_CONSTRUCTORS) { - _match_method_name_and_push_back(class_doc->constructors, &match.constructors); - } - if (search_flags & SEARCH_METHODS) { - _match_method_name_and_push_back(class_doc->methods, &match.methods); - } - if (search_flags & SEARCH_OPERATORS) { - _match_method_name_and_push_back(class_doc->operators, &match.operators); - } - if (search_flags & SEARCH_SIGNALS) { - for (int i = 0; i < class_doc->signals.size(); i++) { - MemberMatch<DocData::MethodDoc> signal; - signal.name = _all_terms_in_name(class_doc->signals[i].name); - signal.keyword = _match_keywords_in_all_terms(class_doc->signals[i].keywords); - if (signal.name || !signal.keyword.is_empty()) { - signal.doc = const_cast<DocData::MethodDoc *>(&class_doc->signals[i]); - match.signals.push_back(signal); - } + if (search_flags & SEARCH_CONSTRUCTORS) { + _match_method_name_and_push_back(class_doc->constructors, &match.constructors); + } + if (search_flags & SEARCH_METHODS) { + _match_method_name_and_push_back(class_doc->methods, &match.methods); + } + if (search_flags & SEARCH_OPERATORS) { + _match_method_name_and_push_back(class_doc->operators, &match.operators); + } + if (search_flags & SEARCH_SIGNALS) { + for (const DocData::MethodDoc &signal_doc : class_doc->signals) { + MemberMatch<DocData::MethodDoc> signal; + signal.name = _all_terms_in_name(signal_doc.name); + signal.keyword = _match_keywords_in_all_terms(signal_doc.keywords); + if (signal.name || !signal.keyword.is_empty()) { + signal.doc = &signal_doc; + match.signals.push_back(signal); } } - if (search_flags & SEARCH_CONSTANTS) { - for (int i = 0; i < class_doc->constants.size(); i++) { - MemberMatch<DocData::ConstantDoc> constant; - constant.name = _all_terms_in_name(class_doc->constants[i].name); - constant.keyword = _match_keywords_in_all_terms(class_doc->constants[i].keywords); - if (constant.name || !constant.keyword.is_empty()) { - constant.doc = const_cast<DocData::ConstantDoc *>(&class_doc->constants[i]); - match.constants.push_back(constant); - } + } + if (search_flags & SEARCH_CONSTANTS) { + for (const DocData::ConstantDoc &constant_doc : class_doc->constants) { + MemberMatch<DocData::ConstantDoc> constant; + constant.name = _all_terms_in_name(constant_doc.name); + constant.keyword = _match_keywords_in_all_terms(constant_doc.keywords); + if (constant.name || !constant.keyword.is_empty()) { + constant.doc = &constant_doc; + match.constants.push_back(constant); } } - if (search_flags & SEARCH_PROPERTIES) { - for (int i = 0; i < class_doc->properties.size(); i++) { - MemberMatch<DocData::PropertyDoc> property; - property.name = _all_terms_in_name(class_doc->properties[i].name); - property.keyword = _match_keywords_in_all_terms(class_doc->properties[i].keywords); - if (property.name || !property.keyword.is_empty()) { - property.doc = const_cast<DocData::PropertyDoc *>(&class_doc->properties[i]); - match.properties.push_back(property); - } + } + if (search_flags & SEARCH_PROPERTIES) { + for (const DocData::PropertyDoc &property_doc : class_doc->properties) { + MemberMatch<DocData::PropertyDoc> property; + property.name = _all_terms_in_name(property_doc.name); + property.keyword = _match_keywords_in_all_terms(property_doc.keywords); + if (property.name || !property.keyword.is_empty()) { + property.doc = &property_doc; + match.properties.push_back(property); } } - if (search_flags & SEARCH_THEME_ITEMS) { - for (int i = 0; i < class_doc->theme_properties.size(); i++) { - MemberMatch<DocData::ThemeItemDoc> theme_property; - theme_property.name = _all_terms_in_name(class_doc->theme_properties[i].name); - theme_property.keyword = _match_keywords_in_all_terms(class_doc->theme_properties[i].keywords); - if (theme_property.name || !theme_property.keyword.is_empty()) { - theme_property.doc = const_cast<DocData::ThemeItemDoc *>(&class_doc->theme_properties[i]); - match.theme_properties.push_back(theme_property); - } + } + if (search_flags & SEARCH_THEME_ITEMS) { + for (const DocData::ThemeItemDoc &theme_property_doc : class_doc->theme_properties) { + MemberMatch<DocData::ThemeItemDoc> theme_property; + theme_property.name = _all_terms_in_name(theme_property_doc.name); + theme_property.keyword = _match_keywords_in_all_terms(theme_property_doc.keywords); + if (theme_property.name || !theme_property.keyword.is_empty()) { + theme_property.doc = &theme_property_doc; + match.theme_properties.push_back(theme_property); } } - if (search_flags & SEARCH_ANNOTATIONS) { - for (int i = 0; i < class_doc->annotations.size(); i++) { - MemberMatch<DocData::MethodDoc> annotation; - annotation.name = _all_terms_in_name(class_doc->annotations[i].name); - annotation.keyword = _match_keywords_in_all_terms(class_doc->annotations[i].keywords); - if (annotation.name || !annotation.keyword.is_empty()) { - annotation.doc = const_cast<DocData::MethodDoc *>(&class_doc->annotations[i]); - match.annotations.push_back(annotation); - } + } + if (search_flags & SEARCH_ANNOTATIONS) { + for (const DocData::MethodDoc &annotation_doc : class_doc->annotations) { + MemberMatch<DocData::MethodDoc> annotation; + annotation.name = _all_terms_in_name(annotation_doc.name); + annotation.keyword = _match_keywords_in_all_terms(annotation_doc.keywords); + if (annotation.name || !annotation.keyword.is_empty()) { + annotation.doc = &annotation_doc; + match.annotations.push_back(annotation); } } } @@ -564,9 +813,11 @@ bool EditorHelpSearch::Runner::_phase_match_classes() { } if (!iterator_stack.is_empty()) { + // Iterate on stack. if (iterator_stack[iterator_stack.size() - 1]) { iterator_stack[iterator_stack.size() - 1] = iterator_stack[iterator_stack.size() - 1]->next(); } + // Drop last element of stack. if (!iterator_stack[iterator_stack.size() - 1]) { iterator_stack.resize(iterator_stack.size() - 1); } @@ -661,36 +912,32 @@ bool EditorHelpSearch::Runner::_phase_member_items() { return false; } + // Pick appropriate parent item if showing hierarchy, otherwise pick root. TreeItem *parent_item = (search_flags & SEARCH_SHOW_HIERARCHY) ? class_items[match.doc->name] : root_item; - bool constructor_created = false; - for (int i = 0; i < match.methods.size(); i++) { - String text = match.methods[i].doc->name; - if (!constructor_created) { - if (match.doc->name == match.methods[i].doc->name) { - text += " " + TTR("(constructors)"); - constructor_created = true; - } - } else { - if (match.doc->name == match.methods[i].doc->name) { - continue; - } - } - _create_method_item(parent_item, match.doc, text, match.methods[i]); + + for (const MemberMatch<DocData::MethodDoc> &constructor_item : match.constructors) { + _create_constructor_item(parent_item, match.doc, constructor_item); } - for (int i = 0; i < match.signals.size(); i++) { - _create_signal_item(parent_item, match.doc, match.signals[i]); + for (const MemberMatch<DocData::MethodDoc> &method_item : match.methods) { + _create_method_item(parent_item, match.doc, method_item); } - for (int i = 0; i < match.constants.size(); i++) { - _create_constant_item(parent_item, match.doc, match.constants[i]); + for (const MemberMatch<DocData::MethodDoc> &operator_item : match.operators) { + _create_operator_item(parent_item, match.doc, operator_item); } - for (int i = 0; i < match.properties.size(); i++) { - _create_property_item(parent_item, match.doc, match.properties[i]); + for (const MemberMatch<DocData::MethodDoc> &signal_item : match.signals) { + _create_signal_item(parent_item, match.doc, signal_item); } - for (int i = 0; i < match.theme_properties.size(); i++) { - _create_theme_property_item(parent_item, match.doc, match.theme_properties[i]); + for (const MemberMatch<DocData::ConstantDoc> &constant_item : match.constants) { + _create_constant_item(parent_item, match.doc, constant_item); } - for (int i = 0; i < match.annotations.size(); i++) { - _create_annotation_item(parent_item, match.doc, match.annotations[i]); + for (const MemberMatch<DocData::PropertyDoc> &property_item : match.properties) { + _create_property_item(parent_item, match.doc, property_item); + } + for (const MemberMatch<DocData::ThemeItemDoc> &theme_property_item : match.theme_properties) { + _create_theme_property_item(parent_item, match.doc, theme_property_item); + } + for (const MemberMatch<DocData::MethodDoc> &annotation_item : match.annotations) { + _create_annotation_item(parent_item, match.doc, annotation_item); } ++iterator_match; @@ -704,7 +951,7 @@ bool EditorHelpSearch::Runner::_phase_select_match() { return true; } -void EditorHelpSearch::Runner::_match_method_name_and_push_back(Vector<DocData::MethodDoc> &p_methods, Vector<MemberMatch<DocData::MethodDoc>> *r_match_methods) { +void EditorHelpSearch::Runner::_match_method_name_and_push_back(Vector<DocData::MethodDoc> &p_methods, LocalVector<MemberMatch<DocData::MethodDoc>> *r_match_methods) { // Constructors, Methods, Operators... for (int i = 0; i < p_methods.size(); i++) { String method_name = (search_flags & SEARCH_CASE_SENSITIVE) ? p_methods[i].name : p_methods[i].name.to_lower(); @@ -765,12 +1012,12 @@ void EditorHelpSearch::Runner::_match_item(TreeItem *p_item, const String &p_tex return; } - float inverse_length = 1.f / float(p_text.length()); + float inverse_length = 1.0f / float(p_text.length()); // Favor types where search term is a substring close to the start of the type. float w = 0.5f; int pos = p_text.findn(term); - float score = (pos > -1) ? 1.0f - w * MIN(1, 3 * pos * inverse_length) : MAX(0.f, .9f - w); + float score = (pos > -1) ? 1.0f - w * MIN(1, 3 * pos * inverse_length) : MAX(0.0f, 0.9f - w); // Favor shorter items: they resemble the search term more. w = 0.1f; @@ -781,7 +1028,8 @@ void EditorHelpSearch::Runner::_match_item(TreeItem *p_item, const String &p_tex score *= 0.9f; } - if (match_highest_score == 0 || score > match_highest_score) { + // Replace current match if term is short as we are searching in reverse. + if (match_highest_score == 0 || score > match_highest_score || (score == match_highest_score && term.length() == 1)) { matched_item = p_item; match_highest_score = score; } @@ -820,6 +1068,29 @@ String EditorHelpSearch::Runner::_build_keywords_tooltip(const String &p_keyword return tooltip.left(-2); } +TreeItem *EditorHelpSearch::Runner::_create_class_hierarchy(const DocData::ClassDoc *p_class_doc, const String &p_matching_keyword, bool p_gray) { + if (p_class_doc->name.is_empty()) { + return nullptr; + } + if (TreeItem **found = class_items.getptr(p_class_doc->name)) { + return *found; + } + + // Ensure parent nodes are created first. + TreeItem *parent_item = root_item; + if (!p_class_doc->inherits.is_empty()) { + if (class_items.has(p_class_doc->inherits)) { + parent_item = class_items[p_class_doc->inherits]; + } else if (const DocData::ClassDoc *found = EditorHelp::get_doc_data()->class_list.getptr(p_class_doc->inherits)) { + parent_item = _create_class_hierarchy(found, String(), true); + } + } + + TreeItem *class_item = _create_class_item(parent_item, p_class_doc, p_gray, p_matching_keyword); + class_items[p_class_doc->name] = class_item; + return class_item; +} + TreeItem *EditorHelpSearch::Runner::_create_class_hierarchy(const ClassMatch &p_match) { if (p_match.doc->name.is_empty()) { return nullptr; @@ -887,6 +1158,8 @@ TreeItem *EditorHelpSearch::Runner::_create_class_item(TreeItem *p_parent, const item->add_button(0, warning_icon, 0, false, TTR("This class is marked as experimental.")); } } + // Cached item might be collapsed. + item->set_collapsed(false); if (p_gray) { item->set_custom_color(0, disabled_color); @@ -902,7 +1175,9 @@ TreeItem *EditorHelpSearch::Runner::_create_class_item(TreeItem *p_parent, const item->set_text(0, p_doc->name + " - " + TTR(vformat("Matches the \"%s\" keyword.", p_matching_keyword))); } - _match_item(item, p_doc->name); + if (!term.is_empty()) { + _match_item(item, p_doc->name); + } for (const String &keyword : p_doc->keywords.split(",")) { _match_item(item, keyword.strip_edges(), true); } @@ -910,44 +1185,73 @@ TreeItem *EditorHelpSearch::Runner::_create_class_item(TreeItem *p_parent, const return item; } -TreeItem *EditorHelpSearch::Runner::_create_method_item(TreeItem *p_parent, const DocData::ClassDoc *p_class_doc, const String &p_text, const MemberMatch<DocData::MethodDoc> &p_match) { +TreeItem *EditorHelpSearch::Runner::_create_constructor_item(TreeItem *p_parent, const DocData::ClassDoc *p_class_doc, const MemberMatch<DocData::MethodDoc> &p_match) { + String tooltip = p_class_doc->name + "("; + String text = p_class_doc->name + "("; + for (int i = 0; i < p_match.doc->arguments.size(); i++) { + const DocData::ArgumentDoc &arg = p_match.doc->arguments[i]; + tooltip += arg.type + " " + arg.name; + text += arg.type; + if (!arg.default_value.is_empty()) { + tooltip += " = " + arg.default_value; + } + if (i < p_match.doc->arguments.size() - 1) { + tooltip += ", "; + text += ", "; + } + } + tooltip += ")"; + tooltip += _build_keywords_tooltip(p_match.doc->keywords); + text += ")"; + return _create_member_item(p_parent, p_class_doc->name, SNAME("MemberConstructor"), p_match.doc->name, text, TTRC("Constructor"), "method", tooltip, p_match.doc->keywords, p_match.doc->is_deprecated, p_match.doc->is_experimental, p_match.name ? String() : p_match.keyword); +} + +TreeItem *EditorHelpSearch::Runner::_create_method_item(TreeItem *p_parent, const DocData::ClassDoc *p_class_doc, const MemberMatch<DocData::MethodDoc> &p_match) { String tooltip = _build_method_tooltip(p_class_doc, p_match.doc); - return _create_member_item(p_parent, p_class_doc->name, "MemberMethod", p_match.doc->name, p_text, TTRC("Method"), "method", tooltip, p_match.doc->keywords, p_match.doc->is_deprecated, p_match.doc->is_experimental, p_match.name ? String() : p_match.keyword); + return _create_member_item(p_parent, p_class_doc->name, SNAME("MemberMethod"), p_match.doc->name, p_match.doc->name, TTRC("Method"), "method", tooltip, p_match.doc->keywords, p_match.doc->is_deprecated, p_match.doc->is_experimental, p_match.name ? String() : p_match.keyword); +} + +TreeItem *EditorHelpSearch::Runner::_create_operator_item(TreeItem *p_parent, const DocData::ClassDoc *p_class_doc, const MemberMatch<DocData::MethodDoc> &p_match) { + String tooltip = _build_method_tooltip(p_class_doc, p_match.doc); + String text = p_match.doc->name; + if (!p_match.doc->arguments.is_empty()) { + text += "(" + p_match.doc->arguments[0].type + ")"; + } + return _create_member_item(p_parent, p_class_doc->name, SNAME("MemberOperator"), p_match.doc->name, text, TTRC("Operator"), "method", tooltip, p_match.doc->keywords, p_match.doc->is_deprecated, p_match.doc->is_experimental, p_match.name ? String() : p_match.keyword); } TreeItem *EditorHelpSearch::Runner::_create_signal_item(TreeItem *p_parent, const DocData::ClassDoc *p_class_doc, const MemberMatch<DocData::MethodDoc> &p_match) { String tooltip = _build_method_tooltip(p_class_doc, p_match.doc); - return _create_member_item(p_parent, p_class_doc->name, "MemberSignal", p_match.doc->name, p_match.doc->name, TTRC("Signal"), "signal", tooltip, p_match.doc->keywords, p_match.doc->is_deprecated, p_match.doc->is_experimental, p_match.name ? String() : p_match.keyword); + return _create_member_item(p_parent, p_class_doc->name, SNAME("MemberSignal"), p_match.doc->name, p_match.doc->name, TTRC("Signal"), "signal", tooltip, p_match.doc->keywords, p_match.doc->is_deprecated, p_match.doc->is_experimental, p_match.name ? String() : p_match.keyword); } TreeItem *EditorHelpSearch::Runner::_create_annotation_item(TreeItem *p_parent, const DocData::ClassDoc *p_class_doc, const MemberMatch<DocData::MethodDoc> &p_match) { String tooltip = _build_method_tooltip(p_class_doc, p_match.doc); // Hide the redundant leading @ symbol. String text = p_match.doc->name.substr(1); - return _create_member_item(p_parent, p_class_doc->name, "MemberAnnotation", p_match.doc->name, text, TTRC("Annotation"), "annotation", tooltip, p_match.doc->keywords, p_match.doc->is_deprecated, p_match.doc->is_experimental, p_match.name ? String() : p_match.keyword); + return _create_member_item(p_parent, p_class_doc->name, SNAME("MemberAnnotation"), p_match.doc->name, text, TTRC("Annotation"), "annotation", tooltip, p_match.doc->keywords, p_match.doc->is_deprecated, p_match.doc->is_experimental, p_match.name ? String() : p_match.keyword); } TreeItem *EditorHelpSearch::Runner::_create_constant_item(TreeItem *p_parent, const DocData::ClassDoc *p_class_doc, const MemberMatch<DocData::ConstantDoc> &p_match) { String tooltip = p_class_doc->name + "." + p_match.doc->name; tooltip += _build_keywords_tooltip(p_match.doc->keywords); - return _create_member_item(p_parent, p_class_doc->name, "MemberConstant", p_match.doc->name, p_match.doc->name, TTRC("Constant"), "constant", tooltip, p_match.doc->keywords, p_match.doc->is_deprecated, p_match.doc->is_experimental, p_match.name ? String() : p_match.keyword); + return _create_member_item(p_parent, p_class_doc->name, SNAME("MemberConstant"), p_match.doc->name, p_match.doc->name, TTRC("Constant"), "constant", tooltip, p_match.doc->keywords, p_match.doc->is_deprecated, p_match.doc->is_experimental, p_match.name ? String() : p_match.keyword); } TreeItem *EditorHelpSearch::Runner::_create_property_item(TreeItem *p_parent, const DocData::ClassDoc *p_class_doc, const MemberMatch<DocData::PropertyDoc> &p_match) { String tooltip = p_match.doc->type + " " + p_class_doc->name + "." + p_match.doc->name; tooltip += "\n " + p_class_doc->name + "." + p_match.doc->setter + "(value) setter"; tooltip += "\n " + p_class_doc->name + "." + p_match.doc->getter + "() getter"; - tooltip += _build_keywords_tooltip(p_match.doc->keywords); - return _create_member_item(p_parent, p_class_doc->name, "MemberProperty", p_match.doc->name, p_match.doc->name, TTRC("Property"), "property", tooltip, p_match.doc->keywords, p_match.doc->is_deprecated, p_match.doc->is_experimental, p_match.name ? String() : p_match.keyword); + return _create_member_item(p_parent, p_class_doc->name, SNAME("MemberProperty"), p_match.doc->name, p_match.doc->name, TTRC("Property"), "property", tooltip, p_match.doc->keywords, p_match.doc->is_deprecated, p_match.doc->is_experimental, p_match.name ? String() : p_match.keyword); } TreeItem *EditorHelpSearch::Runner::_create_theme_property_item(TreeItem *p_parent, const DocData::ClassDoc *p_class_doc, const MemberMatch<DocData::ThemeItemDoc> &p_match) { String tooltip = p_match.doc->type + " " + p_class_doc->name + "." + p_match.doc->name; tooltip += _build_keywords_tooltip(p_match.doc->keywords); - return _create_member_item(p_parent, p_class_doc->name, "MemberTheme", p_match.doc->name, p_match.doc->name, TTRC("Theme Property"), "theme_item", p_match.doc->keywords, tooltip, false, false, p_match.name ? String() : p_match.keyword); + return _create_member_item(p_parent, p_class_doc->name, SNAME("MemberTheme"), p_match.doc->name, p_match.doc->name, TTRC("Theme Property"), "theme_item", p_match.doc->keywords, tooltip, false, false, p_match.name ? String() : p_match.keyword); } -TreeItem *EditorHelpSearch::Runner::_create_member_item(TreeItem *p_parent, const String &p_class_name, const String &p_icon, const String &p_name, const String &p_text, const String &p_type, const String &p_metatype, const String &p_tooltip, const String &p_keywords, bool p_is_deprecated, bool p_is_experimental, const String &p_matching_keyword) { +TreeItem *EditorHelpSearch::Runner::_create_member_item(TreeItem *p_parent, const String &p_class_name, const StringName &p_icon, const String &p_name, const String &p_text, const String &p_type, const String &p_metatype, const String &p_tooltip, const String &p_keywords, bool p_is_deprecated, bool p_is_experimental, const String &p_matching_keyword) { const String item_meta = "class_" + p_metatype + ":" + p_class_name + ":" + p_name; TreeItem *item = nullptr; @@ -978,7 +1282,10 @@ TreeItem *EditorHelpSearch::Runner::_create_member_item(TreeItem *p_parent, cons } item->set_text(0, text); - _match_item(item, p_name); + // Don't match member items for short searches. + if (term.length() > 1 || term == "@") { + _match_item(item, p_name); + } for (const String &keyword : p_keywords.split(",")) { _match_item(item, keyword.strip_edges(), true); } @@ -989,9 +1296,17 @@ TreeItem *EditorHelpSearch::Runner::_create_member_item(TreeItem *p_parent, cons bool EditorHelpSearch::Runner::work(uint64_t slot) { // Return true when the search has been completed, otherwise false. const uint64_t until = OS::get_singleton()->get_ticks_usec() + slot; - while (!_slice()) { - if (OS::get_singleton()->get_ticks_usec() > until) { - return false; + if (term.length() > 1 || term == "@") { + while (!_slice()) { + if (OS::get_singleton()->get_ticks_usec() > until) { + return false; + } + } + } else { + while (!_fill()) { + if (OS::get_singleton()->get_ticks_usec() > until) { + return false; + } } } return true; @@ -1001,7 +1316,7 @@ EditorHelpSearch::Runner::Runner(Control *p_icon_service, Tree *p_results_tree, ui_service(p_icon_service), results_tree(p_results_tree), tree_cache(p_tree_cache), - term((p_search_flags & SEARCH_CASE_SENSITIVE) == 0 ? p_term.strip_edges().to_lower() : p_term.strip_edges()), + term((p_search_flags & SEARCH_CASE_SENSITIVE) == 0 ? p_term.to_lower() : p_term), search_flags(p_search_flags), disabled_color(ui_service->get_theme_color(SNAME("font_disabled_color"), EditorStringName(Editor))) { } diff --git a/editor/editor_help_search.h b/editor/editor_help_search.h index 58061dae4c..b8b3c26b41 100644 --- a/editor/editor_help_search.h +++ b/editor/editor_help_search.h @@ -63,6 +63,7 @@ class EditorHelpSearch : public ConfirmationDialog { Tree *results_tree = nullptr; bool old_search = false; String old_term; + int old_search_flags = 0; class Runner; Ref<Runner> search; @@ -119,26 +120,30 @@ class EditorHelpSearch::Runner : public RefCounted { template <typename T> struct MemberMatch { - T *doc = nullptr; + const T *doc = nullptr; bool name = false; String keyword; + + MemberMatch() {} + MemberMatch(const T *p_doc) : + doc(p_doc) {} }; struct ClassMatch { - DocData::ClassDoc *doc = nullptr; + const DocData::ClassDoc *doc = nullptr; bool name = false; String keyword; - Vector<MemberMatch<DocData::MethodDoc>> constructors; - Vector<MemberMatch<DocData::MethodDoc>> methods; - Vector<MemberMatch<DocData::MethodDoc>> operators; - Vector<MemberMatch<DocData::MethodDoc>> signals; - Vector<MemberMatch<DocData::ConstantDoc>> constants; - Vector<MemberMatch<DocData::PropertyDoc>> properties; - Vector<MemberMatch<DocData::ThemeItemDoc>> theme_properties; - Vector<MemberMatch<DocData::MethodDoc>> annotations; + LocalVector<MemberMatch<DocData::MethodDoc>> constructors; + LocalVector<MemberMatch<DocData::MethodDoc>> methods; + LocalVector<MemberMatch<DocData::MethodDoc>> operators; + LocalVector<MemberMatch<DocData::MethodDoc>> signals; + LocalVector<MemberMatch<DocData::ConstantDoc>> constants; + LocalVector<MemberMatch<DocData::PropertyDoc>> properties; + LocalVector<MemberMatch<DocData::ThemeItemDoc>> theme_properties; + LocalVector<MemberMatch<DocData::MethodDoc>> annotations; bool required() { - return name || !keyword.is_empty() || methods.size() || signals.size() || constants.size() || properties.size() || theme_properties.size() || annotations.size(); + return name || !keyword.is_empty() || !constructors.is_empty() || !methods.is_empty() || !operators.is_empty() || !signals.is_empty() || !constants.is_empty() || !properties.is_empty() || !theme_properties.is_empty() || !annotations.is_empty(); } }; @@ -155,6 +160,7 @@ class EditorHelpSearch::Runner : public RefCounted { LocalVector<RBSet<String, NaturalNoCaseComparator>::Element *> iterator_stack; HashMap<String, ClassMatch> matches; HashMap<String, ClassMatch>::Iterator iterator_match; + LocalVector<Pair<DocData::ClassDoc *, String>> matched_classes; TreeItem *root_item = nullptr; HashMap<String, TreeItem *> class_items; TreeItem *matched_item = nullptr; @@ -165,6 +171,12 @@ class EditorHelpSearch::Runner : public RefCounted { void _populate_cache(); bool _find_or_create_item(TreeItem *p_parent, const String &p_item_meta, TreeItem *&r_item); + bool _fill(); + bool _phase_fill_classes_init(); + bool _phase_fill_classes(); + bool _phase_fill_member_items_init(); + bool _phase_fill_member_items(); + bool _slice(); bool _phase_match_classes_init(); bool _phase_match_classes(); @@ -177,21 +189,25 @@ class EditorHelpSearch::Runner : public RefCounted { String _build_method_tooltip(const DocData::ClassDoc *p_class_doc, const DocData::MethodDoc *p_doc) const; String _build_keywords_tooltip(const String &p_keywords) const; - void _match_method_name_and_push_back(Vector<DocData::MethodDoc> &p_methods, Vector<MemberMatch<DocData::MethodDoc>> *r_match_methods); + void _match_method_name_and_push_back(Vector<DocData::MethodDoc> &p_methods, LocalVector<MemberMatch<DocData::MethodDoc>> *r_match_methods); bool _all_terms_in_name(const String &p_name) const; String _match_keywords_in_all_terms(const String &p_keywords) const; bool _match_string(const String &p_term, const String &p_string) const; String _match_keywords(const String &p_term, const String &p_keywords) const; void _match_item(TreeItem *p_item, const String &p_text, bool p_is_keywords = false); TreeItem *_create_class_hierarchy(const ClassMatch &p_match); + TreeItem *_create_class_hierarchy(const DocData::ClassDoc *p_class_doc, const String &p_matching_keyword, bool p_gray); TreeItem *_create_class_item(TreeItem *p_parent, const DocData::ClassDoc *p_doc, bool p_gray, const String &p_matching_keyword); - TreeItem *_create_method_item(TreeItem *p_parent, const DocData::ClassDoc *p_class_doc, const String &p_text, const MemberMatch<DocData::MethodDoc> &p_match); + TreeItem *_create_category_item(TreeItem *p_parent, const String &p_class, const StringName &p_icon, const String &p_metatype, const String &p_type); + TreeItem *_create_method_item(TreeItem *p_parent, const DocData::ClassDoc *p_class_doc, const MemberMatch<DocData::MethodDoc> &p_match); + TreeItem *_create_constructor_item(TreeItem *p_parent, const DocData::ClassDoc *p_class_doc, const MemberMatch<DocData::MethodDoc> &p_match); + TreeItem *_create_operator_item(TreeItem *p_parent, const DocData::ClassDoc *p_class_doc, const MemberMatch<DocData::MethodDoc> &p_match); TreeItem *_create_signal_item(TreeItem *p_parent, const DocData::ClassDoc *p_class_doc, const MemberMatch<DocData::MethodDoc> &p_match); TreeItem *_create_annotation_item(TreeItem *p_parent, const DocData::ClassDoc *p_class_doc, const MemberMatch<DocData::MethodDoc> &p_match); TreeItem *_create_constant_item(TreeItem *p_parent, const DocData::ClassDoc *p_class_doc, const MemberMatch<DocData::ConstantDoc> &p_match); TreeItem *_create_property_item(TreeItem *p_parent, const DocData::ClassDoc *p_class_doc, const MemberMatch<DocData::PropertyDoc> &p_match); TreeItem *_create_theme_property_item(TreeItem *p_parent, const DocData::ClassDoc *p_class_doc, const MemberMatch<DocData::ThemeItemDoc> &p_match); - TreeItem *_create_member_item(TreeItem *p_parent, const String &p_class_name, const String &p_icon, const String &p_name, const String &p_text, const String &p_type, const String &p_metatype, const String &p_tooltip, const String &p_keywords, bool p_is_deprecated, bool p_is_experimental, const String &p_matching_keyword); + TreeItem *_create_member_item(TreeItem *p_parent, const String &p_class_name, const StringName &p_icon, const String &p_name, const String &p_text, const String &p_type, const String &p_metatype, const String &p_tooltip, const String &p_keywords, bool p_is_deprecated, bool p_is_experimental, const String &p_matching_keyword); public: bool work(uint64_t slot = 100000); diff --git a/editor/editor_inspector.cpp b/editor/editor_inspector.cpp index 199383c391..1e5acce032 100644 --- a/editor/editor_inspector.cpp +++ b/editor/editor_inspector.cpp @@ -4270,7 +4270,7 @@ void EditorInspector::_check_meta_name() { if (meta_name.is_empty()) { validation_panel->set_message(EditorValidationPanel::MSG_ID_DEFAULT, TTR("Metadata name can't be empty."), EditorValidationPanel::MSG_ERROR); - } else if (!meta_name.is_valid_identifier()) { + } else if (!meta_name.is_valid_ascii_identifier()) { validation_panel->set_message(EditorValidationPanel::MSG_ID_DEFAULT, TTR("Metadata name must be a valid identifier."), EditorValidationPanel::MSG_ERROR); } else if (object->has_meta(meta_name)) { validation_panel->set_message(EditorValidationPanel::MSG_ID_DEFAULT, vformat(TTR("Metadata with name \"%s\" already exists."), meta_name), EditorValidationPanel::MSG_ERROR); diff --git a/editor/editor_interface.compat.inc b/editor/editor_interface.compat.inc new file mode 100644 index 0000000000..f5b35931fe --- /dev/null +++ b/editor/editor_interface.compat.inc @@ -0,0 +1,48 @@ +/**************************************************************************/ +/* editor_interface.compat.inc */ +/**************************************************************************/ +/* 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 "editor_interface.h" + +#ifndef DISABLE_DEPRECATED + +void EditorInterface::_popup_node_selector_bind_compat_94323(const Callable &p_callback, const TypedArray<StringName> &p_valid_types) { + popup_node_selector(p_callback, p_valid_types, nullptr); +} + +void EditorInterface::_popup_property_selector_bind_compat_94323(Object *p_object, const Callable &p_callback, const PackedInt32Array &p_type_filter) { + popup_property_selector(p_object, p_callback, p_type_filter, String()); +} + +void EditorInterface::_bind_compatibility_methods() { + ClassDB::bind_compatibility_method(D_METHOD("popup_node_selector", "callback", "valid_types"), &EditorInterface::_popup_node_selector_bind_compat_94323, DEFVAL(TypedArray<StringName>())); + ClassDB::bind_compatibility_method(D_METHOD("popup_property_selector", "object", "callback", "type_filter"), &EditorInterface::_popup_property_selector_bind_compat_94323, DEFVAL(PackedInt32Array())); +} + +#endif diff --git a/editor/editor_interface.cpp b/editor/editor_interface.cpp index 46113ab2cb..86b66ef410 100644 --- a/editor/editor_interface.cpp +++ b/editor/editor_interface.cpp @@ -29,6 +29,7 @@ /**************************************************************************/ #include "editor_interface.h" +#include "editor_interface.compat.inc" #include "editor/editor_command_palette.h" #include "editor/editor_feature_profile.h" @@ -86,6 +87,10 @@ Ref<EditorSettings> EditorInterface::get_editor_settings() const { return EditorSettings::get_singleton(); } +EditorUndoRedoManager *EditorInterface::get_editor_undo_redo() const { + return EditorUndoRedoManager::get_singleton(); +} + TypedArray<Texture2D> EditorInterface::_make_mesh_previews(const TypedArray<Mesh> &p_meshes, int p_preview_size) { Vector<Ref<Mesh>> meshes; @@ -272,7 +277,7 @@ void EditorInterface::set_current_feature_profile(const String &p_profile_name) // Editor dialogs. -void EditorInterface::popup_node_selector(const Callable &p_callback, const TypedArray<StringName> &p_valid_types) { +void EditorInterface::popup_node_selector(const Callable &p_callback, const TypedArray<StringName> &p_valid_types, Node *p_current_value) { // TODO: Should reuse dialog instance instead of creating a fresh one, but need to rework set_valid_types first. if (node_selector) { node_selector->disconnect(SNAME("selected"), callable_mp(this, &EditorInterface::_node_selected).bind(p_callback)); @@ -292,7 +297,7 @@ void EditorInterface::popup_node_selector(const Callable &p_callback, const Type get_base_control()->add_child(node_selector); - node_selector->popup_scenetree_dialog(); + node_selector->popup_scenetree_dialog(p_current_value); const Callable selected_callback = callable_mp(this, &EditorInterface::_node_selected).bind(p_callback); node_selector->connect(SNAME("selected"), selected_callback, CONNECT_DEFERRED); @@ -301,7 +306,7 @@ void EditorInterface::popup_node_selector(const Callable &p_callback, const Type node_selector->connect(SNAME("canceled"), canceled_callback, CONNECT_DEFERRED); } -void EditorInterface::popup_property_selector(Object *p_object, const Callable &p_callback, const PackedInt32Array &p_type_filter) { +void EditorInterface::popup_property_selector(Object *p_object, const Callable &p_callback, const PackedInt32Array &p_type_filter, const String &p_current_value) { // TODO: Should reuse dialog instance instead of creating a fresh one, but need to rework set_type_filter first. if (property_selector) { property_selector->disconnect(SNAME("selected"), callable_mp(this, &EditorInterface::_property_selected).bind(p_callback)); @@ -321,7 +326,7 @@ void EditorInterface::popup_property_selector(Object *p_object, const Callable & get_base_control()->add_child(property_selector); - property_selector->select_property_from_instance(p_object); + property_selector->select_property_from_instance(p_object, p_current_value); const Callable selected_callback = callable_mp(this, &EditorInterface::_property_selected).bind(p_callback); property_selector->connect(SNAME("selected"), selected_callback, CONNECT_DEFERRED); @@ -525,6 +530,7 @@ void EditorInterface::_bind_methods() { ClassDB::bind_method(D_METHOD("get_resource_previewer"), &EditorInterface::get_resource_previewer); ClassDB::bind_method(D_METHOD("get_selection"), &EditorInterface::get_selection); ClassDB::bind_method(D_METHOD("get_editor_settings"), &EditorInterface::get_editor_settings); + ClassDB::bind_method(D_METHOD("get_editor_undo_redo"), &EditorInterface::get_editor_undo_redo); ClassDB::bind_method(D_METHOD("make_mesh_previews", "meshes", "preview_size"), &EditorInterface::_make_mesh_previews); @@ -559,8 +565,8 @@ void EditorInterface::_bind_methods() { // Editor dialogs. - ClassDB::bind_method(D_METHOD("popup_node_selector", "callback", "valid_types"), &EditorInterface::popup_node_selector, DEFVAL(TypedArray<StringName>())); - ClassDB::bind_method(D_METHOD("popup_property_selector", "object", "callback", "type_filter"), &EditorInterface::popup_property_selector, DEFVAL(PackedInt32Array())); + ClassDB::bind_method(D_METHOD("popup_node_selector", "callback", "valid_types", "current_value"), &EditorInterface::popup_node_selector, DEFVAL(TypedArray<StringName>()), DEFVAL(Variant())); + ClassDB::bind_method(D_METHOD("popup_property_selector", "object", "callback", "type_filter", "current_value"), &EditorInterface::popup_property_selector, DEFVAL(PackedInt32Array()), DEFVAL(String())); // Editor docks. diff --git a/editor/editor_interface.h b/editor/editor_interface.h index 3ef4325780..20d66d71f5 100644 --- a/editor/editor_interface.h +++ b/editor/editor_interface.h @@ -45,6 +45,7 @@ class EditorPlugin; class EditorResourcePreview; class EditorSelection; class EditorSettings; +class EditorUndoRedoManager; class FileSystemDock; class Mesh; class Node; @@ -80,6 +81,13 @@ class EditorInterface : public Object { protected: static void _bind_methods(); +#ifndef DISABLE_DEPRECATED + void _popup_node_selector_bind_compat_94323(const Callable &p_callback, const TypedArray<StringName> &p_valid_types = TypedArray<StringName>()); + void _popup_property_selector_bind_compat_94323(Object *p_object, const Callable &p_callback, const PackedInt32Array &p_type_filter = PackedInt32Array()); + + static void _bind_compatibility_methods(); +#endif + public: static EditorInterface *get_singleton() { return singleton; } @@ -93,6 +101,7 @@ public: EditorResourcePreview *get_resource_previewer() const; EditorSelection *get_selection() const; Ref<EditorSettings> get_editor_settings() const; + EditorUndoRedoManager *get_editor_undo_redo() const; Vector<Ref<Texture2D>> make_mesh_previews(const Vector<Ref<Mesh>> &p_meshes, Vector<Transform3D> *p_transforms, int p_preview_size); @@ -126,9 +135,9 @@ public: // Editor dialogs. - void popup_node_selector(const Callable &p_callback, const TypedArray<StringName> &p_valid_types = TypedArray<StringName>()); + void popup_node_selector(const Callable &p_callback, const TypedArray<StringName> &p_valid_types = TypedArray<StringName>(), Node *p_current_value = nullptr); // Must use Vector<int> because exposing Vector<Variant::Type> is not supported. - void popup_property_selector(Object *p_object, const Callable &p_callback, const PackedInt32Array &p_type_filter = PackedInt32Array()); + void popup_property_selector(Object *p_object, const Callable &p_callback, const PackedInt32Array &p_type_filter = PackedInt32Array(), const String &p_current_value = String()); // Editor docks. diff --git a/editor/editor_locale_dialog.cpp b/editor/editor_locale_dialog.cpp index 83f1c70c69..c36792c9e3 100644 --- a/editor/editor_locale_dialog.cpp +++ b/editor/editor_locale_dialog.cpp @@ -408,7 +408,7 @@ EditorLocaleDialog::EditorLocaleDialog() { edit_filters->set_text(TTR("Edit Filters")); edit_filters->set_toggle_mode(true); edit_filters->set_pressed(false); - edit_filters->connect("toggled", callable_mp(this, &EditorLocaleDialog::_edit_filters)); + edit_filters->connect(SceneStringName(toggled), callable_mp(this, &EditorLocaleDialog::_edit_filters)); hb_filter->add_child(edit_filters); } { @@ -416,7 +416,7 @@ EditorLocaleDialog::EditorLocaleDialog() { advanced->set_text(TTR("Advanced")); advanced->set_toggle_mode(true); advanced->set_pressed(false); - advanced->connect("toggled", callable_mp(this, &EditorLocaleDialog::_toggle_advanced)); + advanced->connect(SceneStringName(toggled), callable_mp(this, &EditorLocaleDialog::_toggle_advanced)); hb_filter->add_child(advanced); } vb->add_child(hb_filter); diff --git a/editor/editor_log.cpp b/editor/editor_log.cpp index 0dfbcd0e0d..aec374929e 100644 --- a/editor/editor_log.cpp +++ b/editor/editor_log.cpp @@ -52,10 +52,6 @@ void EditorLog::_error_handler(void *p_self, const char *p_func, const char *p_f err_str = String::utf8(p_file) + ":" + itos(p_line) + " - " + String::utf8(p_error); } - if (p_editor_notify) { - err_str += " (User)"; - } - MessageType message_type = p_type == ERR_HANDLER_WARNING ? MSG_TYPE_WARNING : MSG_TYPE_ERROR; if (!Thread::is_main_thread()) { @@ -398,7 +394,7 @@ void EditorLog::_add_log_line(LogMessage &p_message, bool p_replace_previous) { if (p_replace_previous) { // Force sync last line update (skip if number of unprocessed log messages is too large to avoid editor lag). if (log->get_pending_paragraphs() < 100) { - while (!log->is_ready()) { + while (!log->is_finished()) { ::OS::get_singleton()->delay_usec(1); } } @@ -514,7 +510,7 @@ EditorLog::EditorLog() { collapse_button->set_tooltip_text(TTR("Collapse duplicate messages into one log entry. Shows number of occurrences.")); collapse_button->set_toggle_mode(true); collapse_button->set_pressed(false); - collapse_button->connect("toggled", callable_mp(this, &EditorLog::_set_collapse)); + collapse_button->connect(SceneStringName(toggled), callable_mp(this, &EditorLog::_set_collapse)); hb_tools2->add_child(collapse_button); // Show Search. @@ -525,7 +521,7 @@ EditorLog::EditorLog() { show_search_button->set_pressed(true); show_search_button->set_shortcut(ED_SHORTCUT("editor/open_search", TTR("Focus Search/Filter Bar"), KeyModifierMask::CMD_OR_CTRL | Key::F)); show_search_button->set_shortcut_context(this); - show_search_button->connect("toggled", callable_mp(this, &EditorLog::_set_search_visible)); + show_search_button->connect(SceneStringName(toggled), callable_mp(this, &EditorLog::_set_search_visible)); hb_tools2->add_child(show_search_button); // Message Type Filters. diff --git a/editor/editor_log.h b/editor/editor_log.h index 9c652e912a..899b4a9ac4 100644 --- a/editor/editor_log.h +++ b/editor/editor_log.h @@ -102,7 +102,7 @@ private: toggle_button->add_theme_color_override("icon_color_pressed", Color(1, 1, 1, 1)); toggle_button->set_focus_mode(FOCUS_NONE); // When toggled call the callback and pass the MessageType this button is for. - toggle_button->connect("toggled", p_toggled_callback.bind(type)); + toggle_button->connect(SceneStringName(toggled), p_toggled_callback.bind(type)); } int get_message_count() { diff --git a/editor/editor_node.cpp b/editor/editor_node.cpp index a8f8edaae8..59c8b63d6d 100644 --- a/editor/editor_node.cpp +++ b/editor/editor_node.cpp @@ -167,6 +167,10 @@ #include "modules/modules_enabled.gen.h" // For gdscript, mono. +#if defined(GLES3_ENABLED) +#include "drivers/gles3/rasterizer_gles3.h" +#endif + EditorNode *EditorNode::singleton = nullptr; static const String EDITOR_NODE_CONFIG_SECTION = "EditorNode"; @@ -481,6 +485,9 @@ void EditorNode::_gdextensions_reloaded() { // In case the developer is inspecting an object that will be changed by the reload. InspectorDock::get_inspector_singleton()->update_tree(); + // Reload script editor to revalidate GDScript if classes are added or removed. + ScriptEditor::get_singleton()->reload_scripts(true); + // Regenerate documentation. EditorHelp::generate_doc(); } @@ -702,6 +709,8 @@ void EditorNode::_notification(int p_what) { last_system_base_color = DisplayServer::get_singleton()->get_base_color(); DisplayServer::get_singleton()->set_system_theme_change_callback(callable_mp(this, &EditorNode::_check_system_theme_changed)); + get_viewport()->connect("size_changed", callable_mp(this, &EditorNode::_viewport_resized)); + /* DO NOT LOAD SCENES HERE, WAIT FOR FILE SCANNING AND REIMPORT TO COMPLETE */ } break; @@ -730,6 +739,7 @@ void EditorNode::_notification(int p_what) { FileAccess::set_file_close_fail_notify_callback(nullptr); log->deinit(); // Do not get messages anymore. editor_data.clear_edited_scenes(); + get_viewport()->disconnect("size_changed", callable_mp(this, &EditorNode::_viewport_resized)); } break; case NOTIFICATION_READY: { @@ -737,6 +747,7 @@ void EditorNode::_notification(int p_what) { RenderingServer::get_singleton()->viewport_set_disable_2d(get_scene_root()->get_viewport_rid(), true); RenderingServer::get_singleton()->viewport_set_environment_mode(get_viewport()->get_viewport_rid(), RenderingServer::VIEWPORT_ENVIRONMENT_DISABLED); + DisplayServer::get_singleton()->screen_set_keep_on(EDITOR_GET("interface/editor/keep_screen_on")); feature_profile_manager->notify_changed(); @@ -835,6 +846,7 @@ void EditorNode::_notification(int p_what) { if (EditorSettings::get_singleton()->check_changed_settings_in_group("interface/editor")) { _update_update_spinner(); _update_vsync_mode(); + DisplayServer::get_singleton()->screen_set_keep_on(EDITOR_GET("interface/editor/keep_screen_on")); } #if defined(MODULE_GDSCRIPT_ENABLED) || defined(MODULE_MONO_ENABLED) @@ -1073,30 +1085,32 @@ void EditorNode::_resources_reimporting(const Vector<String> &p_resources) { // the inherited scene. Then, get_modified_properties_for_node will return the mesh property, // which will trigger a recopy of the previous mesh, preventing the reload. scenes_modification_table.clear(); - List<String> scenes; + scenes_reimported.clear(); + resources_reimported.clear(); + EditorFileSystem *editor_file_system = EditorFileSystem::get_singleton(); for (const String &res_path : p_resources) { - if (ResourceLoader::get_resource_type(res_path) == "PackedScene") { - scenes.push_back(res_path); + // It's faster to use EditorFileSystem::get_file_type than fetching the resource type from disk. + // This makes a big difference when reimporting many resources. + String file_type = editor_file_system->get_file_type(res_path); + if (file_type.is_empty()) { + file_type = ResourceLoader::get_resource_type(res_path); + } + if (file_type == "PackedScene") { + scenes_reimported.push_back(res_path); + } else { + resources_reimported.push_back(res_path); } } - if (scenes.size() > 0) { - preload_reimporting_with_path_in_edited_scenes(scenes); + if (scenes_reimported.size() > 0) { + preload_reimporting_with_path_in_edited_scenes(scenes_reimported); } } void EditorNode::_resources_reimported(const Vector<String> &p_resources) { - List<String> scenes; int current_tab = scene_tabs->get_current_tab(); - for (const String &res_path : p_resources) { - String file_type = ResourceLoader::get_resource_type(res_path); - if (file_type == "PackedScene") { - scenes.push_back(res_path); - // Reload later if needed, first go with normal resources. - continue; - } - + for (const String &res_path : resources_reimported) { if (!ResourceCache::has(res_path)) { // Not loaded, no need to reload. continue; @@ -1110,16 +1124,20 @@ void EditorNode::_resources_reimported(const Vector<String> &p_resources) { // Editor may crash when related animation is playing while re-importing GLTF scene, stop it in advance. AnimationPlayer *ap = AnimationPlayerEditor::get_singleton()->get_player(); - if (ap && scenes.size() > 0) { + if (ap && scenes_reimported.size() > 0) { ap->stop(true); } - for (const String &E : scenes) { + for (const String &E : scenes_reimported) { reload_scene(E); } reload_instances_with_path_in_edited_scenes(); + scenes_modification_table.clear(); + scenes_reimported.clear(); + resources_reimported.clear(); + _set_current_scene_nocheck(current_tab); } @@ -1234,6 +1252,13 @@ void EditorNode::_reload_project_settings() { void EditorNode::_vp_resized() { } +void EditorNode::_viewport_resized() { + Window *w = get_window(); + if (w) { + was_window_windowed_last = w->get_mode() == Window::MODE_WINDOWED; + } +} + void EditorNode::_titlebar_resized() { DisplayServer::get_singleton()->window_set_window_buttons_offset(Vector2i(title_bar->get_global_position().y + title_bar->get_size().y / 2, title_bar->get_global_position().y + title_bar->get_size().y / 2), DisplayServer::MAIN_WINDOW_ID); const Vector3i &margin = DisplayServer::get_singleton()->window_get_safe_title_margins(DisplayServer::MAIN_WINDOW_ID); @@ -2943,7 +2968,7 @@ void EditorNode::_menu_option_confirm(int p_option, bool p_confirmed) { ERR_PRINT("Failed to load scene"); } editor_data.move_edited_scene_to_index(cur_idx); - EditorUndoRedoManager::get_singleton()->clear_history(false, editor_data.get_current_edited_scene_history_id()); + EditorUndoRedoManager::get_singleton()->clear_history(editor_data.get_current_edited_scene_history_id(), false); scene_tabs->set_current_tab(cur_idx); } break; @@ -3945,7 +3970,7 @@ void EditorNode::_set_current_scene_nocheck(int p_idx) { editor_folding.load_scene_folding(editor_data.get_edited_scene_root(p_idx), editor_data.get_scene_path(p_idx)); } - EditorUndoRedoManager::get_singleton()->clear_history(false, editor_data.get_scene_history_id(p_idx)); + EditorUndoRedoManager::get_singleton()->clear_history(editor_data.get_scene_history_id(p_idx), false); } Dictionary state = editor_data.restore_edited_scene_state(editor_selection, &editor_history); @@ -4055,7 +4080,7 @@ Error EditorNode::load_scene(const String &p_scene, bool p_ignore_broken_deps, b _set_current_scene(idx); } } else { - EditorUndoRedoManager::get_singleton()->clear_history(false, editor_data.get_current_edited_scene_history_id()); + EditorUndoRedoManager::get_singleton()->clear_history(editor_data.get_current_edited_scene_history_id(), false); } dependency_errors.clear(); @@ -4931,7 +4956,9 @@ bool EditorNode::is_object_of_custom_type(const Object *p_object, const StringNa } void EditorNode::progress_add_task(const String &p_task, const String &p_label, int p_steps, bool p_can_cancel) { - if (singleton->cmdline_export_mode) { + if (!singleton) { + return; + } else if (singleton->cmdline_export_mode) { print_line(p_task + ": begin: " + p_label + " steps: " + itos(p_steps)); } else if (singleton->progress_dialog) { singleton->progress_dialog->add_task(p_task, p_label, p_steps, p_can_cancel); @@ -4939,7 +4966,9 @@ void EditorNode::progress_add_task(const String &p_task, const String &p_label, } bool EditorNode::progress_task_step(const String &p_task, const String &p_state, int p_step, bool p_force_refresh) { - if (singleton->cmdline_export_mode) { + if (!singleton) { + return false; + } else if (singleton->cmdline_export_mode) { print_line("\t" + p_task + ": step " + itos(p_step) + ": " + p_state); return false; } else if (singleton->progress_dialog) { @@ -4950,7 +4979,9 @@ bool EditorNode::progress_task_step(const String &p_task, const String &p_state, } void EditorNode::progress_end_task(const String &p_task) { - if (singleton->cmdline_export_mode) { + if (!singleton) { + return; + } else if (singleton->cmdline_export_mode) { print_line(p_task + ": end"); } else if (singleton->progress_dialog) { singleton->progress_dialog->end_task(p_task); @@ -4989,8 +5020,8 @@ String EditorNode::_get_system_info() const { #ifdef LINUXBSD_ENABLED const String display_server = OS::get_singleton()->get_environment("XDG_SESSION_TYPE").capitalize().replace(" ", ""); // `replace` is necessary, because `capitalize` introduces a whitespace between "x" and "11". #endif // LINUXBSD_ENABLED - String driver_name = GLOBAL_GET("rendering/rendering_device/driver"); - String rendering_method = GLOBAL_GET("rendering/renderer/rendering_method"); + String driver_name = OS::get_singleton()->get_current_rendering_driver_name().to_lower(); + String rendering_method = OS::get_singleton()->get_current_rendering_method().to_lower(); const String rendering_device_name = RenderingServer::get_singleton()->get_video_adapter_name(); @@ -5026,12 +5057,23 @@ String EditorNode::_get_system_info() const { rendering_method = "Mobile"; } else if (rendering_method == "gl_compatibility") { rendering_method = "Compatibility"; - driver_name = GLOBAL_GET("rendering/gl_compatibility/driver"); } if (driver_name == "vulkan") { driver_name = "Vulkan"; - } else if (driver_name.begins_with("opengl3")) { - driver_name = "GLES3"; + } else if (driver_name == "d3d12") { + driver_name = "Direct3D 12"; +#if defined(GLES3_ENABLED) + } else if (driver_name == "opengl3_angle") { + driver_name = "OpenGL ES 3/ANGLE"; + } else if (driver_name == "opengl3_es") { + driver_name = "OpenGL ES 3"; + } else if (driver_name == "opengl3") { + if (RasterizerGLES3::is_gles_over_gl()) { + driver_name = "OpenGL 3"; + } else { + driver_name = "OpenGL ES 3"; + } +#endif } else if (driver_name == "metal") { driver_name = "Metal"; } @@ -5214,6 +5256,7 @@ void EditorNode::_save_editor_layout() { editor_dock_manager->save_docks_to_config(config, "docks"); _save_open_scenes_to_config(config); _save_central_editor_layout_to_config(config); + _save_window_settings_to_config(config, "EditorWindow"); editor_data.get_plugin_window_layout(config); config->save(EditorPaths::get_singleton()->get_project_settings_dir().path_join("editor_layout.cfg")); @@ -5239,6 +5282,8 @@ void EditorNode::save_editor_layout_delayed() { } void EditorNode::_load_editor_layout() { + EditorProgress ep("loading_editor_layout", TTR("Loading editor"), 5); + ep.step(TTR("Loading editor layout..."), 0, true); Ref<ConfigFile> config; config.instantiate(); Error err = config->load(EditorPaths::get_singleton()->get_project_settings_dir().path_join("editor_layout.cfg")); @@ -5260,11 +5305,19 @@ void EditorNode::_load_editor_layout() { return; } + ep.step(TTR("Loading docks..."), 1, true); editor_dock_manager->load_docks_from_config(config, "docks"); + + ep.step(TTR("Reopening scenes..."), 2, true); _load_open_scenes_from_config(config); + + ep.step(TTR("Loading central editor layout..."), 3, true); _load_central_editor_layout_from_config(config); + ep.step(TTR("Loading plugin window layout..."), 4, true); editor_data.set_plugin_window_layout(config); + + ep.step(TTR("Editor layout ready."), 5, true); } void EditorNode::_save_central_editor_layout_to_config(Ref<ConfigFile> p_config_file) { @@ -5323,6 +5376,38 @@ void EditorNode::_load_central_editor_layout_from_config(Ref<ConfigFile> p_confi } } +void EditorNode::_save_window_settings_to_config(Ref<ConfigFile> p_layout, const String &p_section) { + Window *w = get_window(); + if (w) { + p_layout->set_value(p_section, "screen", w->get_current_screen()); + + Window::Mode mode = w->get_mode(); + switch (mode) { + case Window::MODE_WINDOWED: + p_layout->set_value(p_section, "mode", "windowed"); + p_layout->set_value(p_section, "size", w->get_size()); + break; + case Window::MODE_FULLSCREEN: + case Window::MODE_EXCLUSIVE_FULLSCREEN: + p_layout->set_value(p_section, "mode", "fullscreen"); + break; + case Window::MODE_MINIMIZED: + if (was_window_windowed_last) { + p_layout->set_value(p_section, "mode", "windowed"); + p_layout->set_value(p_section, "size", w->get_size()); + } else { + p_layout->set_value(p_section, "mode", "maximized"); + } + break; + default: + p_layout->set_value(p_section, "mode", "maximized"); + break; + } + + p_layout->set_value(p_section, "position", w->get_position()); + } +} + void EditorNode::_load_open_scenes_from_config(Ref<ConfigFile> p_layout) { if (!bool(EDITOR_GET("interface/scene_tabs/restore_scenes_on_load"))) { return; @@ -5867,7 +5952,7 @@ void EditorNode::reload_scene(const String &p_path) { bool is_unsaved = EditorUndoRedoManager::get_singleton()->is_history_unsaved(current_history_id); // Scene is not open, so at it might be instantiated. We'll refresh the whole scene later. - EditorUndoRedoManager::get_singleton()->clear_history(false, current_history_id); + EditorUndoRedoManager::get_singleton()->clear_history(current_history_id, false); if (is_unsaved) { EditorUndoRedoManager::get_singleton()->set_history_as_unsaved(current_history_id); } @@ -5886,7 +5971,7 @@ void EditorNode::reload_scene(const String &p_path) { // Adjust index so tab is back a the previous position. editor_data.move_edited_scene_to_index(scene_idx); - EditorUndoRedoManager::get_singleton()->clear_history(false, editor_data.get_scene_history_id(scene_idx)); + EditorUndoRedoManager::get_singleton()->clear_history(editor_data.get_scene_history_id(scene_idx), false); // Recover the tab. scene_tabs->set_current_tab(current_tab); @@ -6031,7 +6116,7 @@ void EditorNode::reload_instances_with_path_in_edited_scenes() { bool is_unsaved = EditorUndoRedoManager::get_singleton()->is_history_unsaved(current_history_id); // Clear the history for this affected tab. - EditorUndoRedoManager::get_singleton()->clear_history(false, current_history_id); + EditorUndoRedoManager::get_singleton()->clear_history(current_history_id, false); // Update the version editor_data.is_scene_changed(current_scene_idx); @@ -6328,8 +6413,6 @@ void EditorNode::reload_instances_with_path_in_edited_scenes() { editor_data.restore_edited_scene_state(editor_selection, &editor_history); - scenes_modification_table.clear(); - progress.step(TTR("Reloading done."), editor_data.get_edited_scene_count()); } @@ -6829,10 +6912,10 @@ EditorNode::EditorNode() { import_shader_file.instantiate(); ResourceFormatImporter::get_singleton()->add_importer(import_shader_file); - Ref<ResourceImporterScene> import_scene = memnew(ResourceImporterScene(false, true)); + Ref<ResourceImporterScene> import_scene = memnew(ResourceImporterScene("PackedScene", true)); ResourceFormatImporter::get_singleton()->add_importer(import_scene); - Ref<ResourceImporterScene> import_animation = memnew(ResourceImporterScene(true, true)); + Ref<ResourceImporterScene> import_animation = memnew(ResourceImporterScene("AnimationLibrary", true)); ResourceFormatImporter::get_singleton()->add_importer(import_animation); { @@ -7299,11 +7382,9 @@ EditorNode::EditorNode() { settings_menu->set_item_tooltip(-1, TTR("Screenshots are stored in the user data folder (\"user://\").")); -#ifndef ANDROID_ENABLED ED_SHORTCUT_AND_COMMAND("editor/fullscreen_mode", TTR("Toggle Fullscreen"), KeyModifierMask::SHIFT | Key::F11); ED_SHORTCUT_OVERRIDE("editor/fullscreen_mode", "macos", KeyModifierMask::META | KeyModifierMask::CTRL | Key::F); settings_menu->add_shortcut(ED_GET_SHORTCUT("editor/fullscreen_mode"), SETTINGS_TOGGLE_FULLSCREEN); -#endif settings_menu->add_separator(); #ifndef ANDROID_ENABLED @@ -7319,9 +7400,7 @@ EditorNode::EditorNode() { #endif settings_menu->add_item(TTR("Manage Editor Features..."), SETTINGS_MANAGE_FEATURE_PROFILES); -#ifndef ANDROID_ENABLED settings_menu->add_item(TTR("Manage Export Templates..."), SETTINGS_MANAGE_EXPORT_TEMPLATES); -#endif #if !defined(ANDROID_ENABLED) && !defined(WEB_ENABLED) settings_menu->add_item(TTR("Configure FBX Importer..."), SETTINGS_MANAGE_FBX_IMPORTER); #endif @@ -7840,6 +7919,9 @@ EditorNode::EditorNode() { ED_SHORTCUT_AND_COMMAND("editor/editor_next", TTR("Open the next Editor")); ED_SHORTCUT_AND_COMMAND("editor/editor_prev", TTR("Open the previous Editor")); + // Apply setting presets in case the editor_settings file is missing values. + EditorSettingsDialog::update_navigation_preset(); + screenshot_timer = memnew(Timer); screenshot_timer->set_one_shot(true); screenshot_timer->set_wait_time(settings_menu->get_submenu_popup_delay() + 0.1f); diff --git a/editor/editor_node.h b/editor/editor_node.h index 222b1cf90c..bdf9b26a7a 100644 --- a/editor/editor_node.h +++ b/editor/editor_node.h @@ -499,6 +499,8 @@ private: SurfaceUpgradeDialog *surface_upgrade_dialog = nullptr; bool run_surface_upgrade_tool = false; + bool was_window_windowed_last = false; + static EditorBuildCallback build_callbacks[MAX_BUILD_CALLBACKS]; static EditorPluginInitializeCallback plugin_init_callbacks[MAX_INIT_CALLBACKS]; static int build_callback_count; @@ -580,6 +582,7 @@ private: void _show_messages(); void _vp_resized(); void _titlebar_resized(); + void _viewport_resized(); void _update_undo_redo_allowed(); @@ -651,6 +654,8 @@ private: void _save_central_editor_layout_to_config(Ref<ConfigFile> p_config_file); void _load_central_editor_layout_from_config(Ref<ConfigFile> p_config_file); + void _save_window_settings_to_config(Ref<ConfigFile> p_layout, const String &p_section); + void _save_open_scenes_to_config(Ref<ConfigFile> p_layout); void _load_open_scenes_from_config(Ref<ConfigFile> p_layout); @@ -845,6 +850,8 @@ public: }; HashMap<int, SceneModificationsEntry> scenes_modification_table; + List<String> scenes_reimported; + List<String> resources_reimported; void update_node_from_node_modification_entry(Node *p_node, ModificationNodeEntry &p_node_modification); diff --git a/editor/editor_paths.cpp b/editor/editor_paths.cpp index be511452a6..7f24e8fd2e 100644 --- a/editor/editor_paths.cpp +++ b/editor/editor_paths.cpp @@ -71,7 +71,11 @@ String EditorPaths::get_export_templates_dir() const { } String EditorPaths::get_debug_keystore_path() const { +#ifdef ANDROID_ENABLED + return "assets://keystores/debug.keystore"; +#else return get_data_dir().path_join("keystores/debug.keystore"); +#endif } String EditorPaths::get_project_settings_dir() const { diff --git a/editor/editor_properties_array_dict.cpp b/editor/editor_properties_array_dict.cpp index d58d0520cc..f5d016629f 100644 --- a/editor/editor_properties_array_dict.cpp +++ b/editor/editor_properties_array_dict.cpp @@ -644,6 +644,8 @@ void EditorPropertyArray::_notification(int p_what) { case NOTIFICATION_THEME_CHANGED: case NOTIFICATION_ENTER_TREE: { change_type->clear(); + change_type->add_icon_item(get_editor_theme_icon(SNAME("Remove")), TTR("Remove Item"), Variant::VARIANT_MAX); + change_type->add_separator(); for (int i = 0; i < Variant::VARIANT_MAX; i++) { if (i == Variant::CALLABLE || i == Variant::SIGNAL || i == Variant::RID) { // These types can't be constructed or serialized properly, so skip them. @@ -653,8 +655,6 @@ void EditorPropertyArray::_notification(int p_what) { String type = Variant::get_type_name(Variant::Type(i)); change_type->add_icon_item(get_editor_theme_icon(type), type, i); } - change_type->add_separator(); - change_type->add_icon_item(get_editor_theme_icon(SNAME("Remove")), TTR("Remove Item"), Variant::VARIANT_MAX); if (button_add_item) { button_add_item->set_icon(get_editor_theme_icon(SNAME("Add"))); @@ -1117,6 +1117,8 @@ void EditorPropertyDictionary::_notification(int p_what) { case NOTIFICATION_THEME_CHANGED: case NOTIFICATION_ENTER_TREE: { change_type->clear(); + change_type->add_icon_item(get_editor_theme_icon(SNAME("Remove")), TTR("Remove Item"), Variant::VARIANT_MAX); + change_type->add_separator(); for (int i = 0; i < Variant::VARIANT_MAX; i++) { if (i == Variant::CALLABLE || i == Variant::SIGNAL || i == Variant::RID) { // These types can't be constructed or serialized properly, so skip them. @@ -1126,8 +1128,6 @@ void EditorPropertyDictionary::_notification(int p_what) { String type = Variant::get_type_name(Variant::Type(i)); change_type->add_icon_item(get_editor_theme_icon(type), type, i); } - change_type->add_separator(); - change_type->add_icon_item(get_editor_theme_icon(SNAME("Remove")), TTR("Remove Item"), Variant::VARIANT_MAX); if (button_add_item) { button_add_item->set_icon(get_editor_theme_icon(SNAME("Add"))); diff --git a/editor/editor_properties_vector.cpp b/editor/editor_properties_vector.cpp index 365cbc6ec8..9ff8bf674d 100644 --- a/editor/editor_properties_vector.cpp +++ b/editor/editor_properties_vector.cpp @@ -130,9 +130,11 @@ void EditorPropertyVectorN::_notification(int p_what) { switch (p_what) { case NOTIFICATION_READY: { if (linked->is_visible()) { - const String key = vformat("%s:%s", get_edited_object()->get_class(), get_edited_property()); - linked->set_pressed_no_signal(EditorSettings::get_singleton()->get_project_metadata("linked_properties", key, true)); - _update_ratio(); + if (get_edited_object()) { + const String key = vformat("%s:%s", get_edited_object()->get_class(), get_edited_property()); + linked->set_pressed_no_signal(EditorSettings::get_singleton()->get_project_metadata("linked_properties", key, true)); + _update_ratio(); + } } } break; @@ -236,7 +238,7 @@ EditorPropertyVectorN::EditorPropertyVectorN(Variant::Type p_type, bool p_force_ linked->set_stretch_mode(TextureButton::STRETCH_KEEP_CENTERED); linked->set_tooltip_text(TTR("Lock/Unlock Component Ratio")); linked->connect(SceneStringName(pressed), callable_mp(this, &EditorPropertyVectorN::_update_ratio)); - linked->connect(SNAME("toggled"), callable_mp(this, &EditorPropertyVectorN::_store_link)); + linked->connect(SceneStringName(toggled), callable_mp(this, &EditorPropertyVectorN::_store_link)); hb->add_child(linked); add_child(hb); diff --git a/editor/editor_resource_picker.cpp b/editor/editor_resource_picker.cpp index 8935b9ad8a..f20dd992bb 100644 --- a/editor/editor_resource_picker.cpp +++ b/editor/editor_resource_picker.cpp @@ -175,6 +175,13 @@ void EditorResourcePicker::_file_quick_selected() { _file_selected(quick_open->get_selected()); } +void EditorResourcePicker::_resource_saved(Object *p_resource) { + if (edited_resource.is_valid() && p_resource == edited_resource.ptr()) { + emit_signal(SNAME("resource_changed"), edited_resource); + _update_resource(); + } +} + void EditorResourcePicker::_update_menu() { _update_menu_items(); @@ -408,6 +415,10 @@ void EditorResourcePicker::_edit_menu_cbk(int p_which) { if (edited_resource.is_null()) { return; } + Callable resource_saved = callable_mp(this, &EditorResourcePicker::_resource_saved); + if (!EditorNode::get_singleton()->is_connected("resource_saved", resource_saved)) { + EditorNode::get_singleton()->connect("resource_saved", resource_saved); + } EditorNode::get_singleton()->save_resource_as(edited_resource); } break; @@ -833,6 +844,13 @@ void EditorResourcePicker::_notification(int p_what) { assign_button->queue_redraw(); } } break; + + case NOTIFICATION_EXIT_TREE: { + Callable resource_saved = callable_mp(this, &EditorResourcePicker::_resource_saved); + if (EditorNode::get_singleton()->is_connected("resource_saved", resource_saved)) { + EditorNode::get_singleton()->disconnect("resource_saved", resource_saved); + } + } break; } } diff --git a/editor/editor_resource_picker.h b/editor/editor_resource_picker.h index 28229e6b37..05e392da2c 100644 --- a/editor/editor_resource_picker.h +++ b/editor/editor_resource_picker.h @@ -91,6 +91,8 @@ class EditorResourcePicker : public HBoxContainer { void _file_quick_selected(); void _file_selected(const String &p_path); + void _resource_saved(Object *p_resource); + void _update_menu(); void _update_menu_items(); void _edit_menu_cbk(int p_which); diff --git a/editor/editor_resource_preview.cpp b/editor/editor_resource_preview.cpp index 71865f7e8c..956fdc5cfa 100644 --- a/editor/editor_resource_preview.cpp +++ b/editor/editor_resource_preview.cpp @@ -221,7 +221,9 @@ void EditorResourcePreview::_generate_preview(Ref<ImageTexture> &r_texture, Ref< r_small_texture->set_image(small_image); } - break; + if (generated.is_valid()) { + break; + } } if (!p_item.resource.is_valid()) { diff --git a/editor/editor_run_native.cpp b/editor/editor_run_native.cpp index 5d378820ae..e0e1ef6d19 100644 --- a/editor/editor_run_native.cpp +++ b/editor/editor_run_native.cpp @@ -141,7 +141,7 @@ Error EditorRunNative::start_run_native(int p_id) { emit_signal(SNAME("native_run"), preset); - int flags = 0; + BitField<EditorExportPlatform::DebugFlags> flags = 0; bool deploy_debug_remote = is_deploy_debug_remote_enabled(); bool deploy_dumb = EditorSettings::get_singleton()->get_project_metadata("debug_options", "run_file_server", false); @@ -149,16 +149,16 @@ Error EditorRunNative::start_run_native(int p_id) { bool debug_navigation = EditorSettings::get_singleton()->get_project_metadata("debug_options", "run_debug_navigation", false); if (deploy_debug_remote) { - flags |= EditorExportPlatform::DEBUG_FLAG_REMOTE_DEBUG; + flags.set_flag(EditorExportPlatform::DEBUG_FLAG_REMOTE_DEBUG); } if (deploy_dumb) { - flags |= EditorExportPlatform::DEBUG_FLAG_DUMB_CLIENT; + flags.set_flag(EditorExportPlatform::DEBUG_FLAG_DUMB_CLIENT); } if (debug_collisions) { - flags |= EditorExportPlatform::DEBUG_FLAG_VIEW_COLLISIONS; + flags.set_flag(EditorExportPlatform::DEBUG_FLAG_VIEW_COLLISIONS); } if (debug_navigation) { - flags |= EditorExportPlatform::DEBUG_FLAG_VIEW_NAVIGATION; + flags.set_flag(EditorExportPlatform::DEBUG_FLAG_VIEW_NAVIGATION); } eep->clear_messages(); diff --git a/editor/editor_settings.cpp b/editor/editor_settings.cpp index b9d530353c..b5c11b574e 100644 --- a/editor/editor_settings.cpp +++ b/editor/editor_settings.cpp @@ -452,6 +452,7 @@ void EditorSettings::_load_defaults(Ref<ConfigFile> p_extra_config) { _initial_set("interface/editor/separate_distraction_mode", false); _initial_set("interface/editor/automatically_open_screenshots", true); EDITOR_SETTING_USAGE(Variant::BOOL, PROPERTY_HINT_NONE, "interface/editor/single_window_mode", false, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_RESTART_IF_CHANGED) + _initial_set("interface/editor/remember_window_size_and_position", true); _initial_set("interface/editor/mouse_extra_buttons_navigate_history", true); _initial_set("interface/editor/save_each_scene_on_quit", true); // Regression EDITOR_SETTING(Variant::BOOL, PROPERTY_HINT_NONE, "interface/editor/save_on_focus_loss", false, "") @@ -466,6 +467,7 @@ void EditorSettings::_load_defaults(Ref<ConfigFile> p_extra_config) { EDITOR_SETTING(Variant::INT, PROPERTY_HINT_ENUM, "interface/editor/show_update_spinner", 0, "Auto (Disabled),Enabled,Disabled") #endif + _initial_set("interface/editor/keep_screen_on", false); EDITOR_SETTING_USAGE(Variant::INT, PROPERTY_HINT_RANGE, "interface/editor/low_processor_mode_sleep_usec", 6900, "1,100000,1", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_RESTART_IF_CHANGED) // Default unfocused usec sleep is for 10 FPS. Allow an unfocused FPS limit // as low as 1 FPS for those who really need low power usage (but don't need @@ -729,14 +731,14 @@ void EditorSettings::_load_defaults(Ref<ConfigFile> p_extra_config) { // 3D: Navigation _initial_set("editors/3d/navigation/invert_x_axis", false); _initial_set("editors/3d/navigation/invert_y_axis", false); - EDITOR_SETTING(Variant::INT, PROPERTY_HINT_ENUM, "editors/3d/navigation/navigation_scheme", 0, "Godot,Maya,Modo") + EDITOR_SETTING(Variant::INT, PROPERTY_HINT_ENUM, "editors/3d/navigation/navigation_scheme", 0, "Godot,Maya,Modo,Custom") + EDITOR_SETTING(Variant::INT, PROPERTY_HINT_ENUM, "editors/3d/navigation/orbit_mouse_button", 1, "Left Mouse,Middle Mouse,Right Mouse") + EDITOR_SETTING(Variant::INT, PROPERTY_HINT_ENUM, "editors/3d/navigation/pan_mouse_button", 1, "Left Mouse,Middle Mouse,Right Mouse") + EDITOR_SETTING(Variant::INT, PROPERTY_HINT_ENUM, "editors/3d/navigation/zoom_mouse_button", 1, "Left Mouse,Middle Mouse,Right Mouse") EDITOR_SETTING(Variant::INT, PROPERTY_HINT_ENUM, "editors/3d/navigation/zoom_style", 0, "Vertical,Horizontal") _initial_set("editors/3d/navigation/emulate_numpad", false); _initial_set("editors/3d/navigation/emulate_3_button_mouse", false); - EDITOR_SETTING(Variant::INT, PROPERTY_HINT_ENUM, "editors/3d/navigation/orbit_modifier", 0, "None,Shift,Alt,Meta,Ctrl") - EDITOR_SETTING(Variant::INT, PROPERTY_HINT_ENUM, "editors/3d/navigation/pan_modifier", 1, "None,Shift,Alt,Meta,Ctrl") - EDITOR_SETTING(Variant::INT, PROPERTY_HINT_ENUM, "editors/3d/navigation/zoom_modifier", 4, "None,Shift,Alt,Meta,Ctrl") _initial_set("editors/3d/navigation/warped_mouse_panning", true); // 3D: Navigation feel @@ -766,6 +768,7 @@ void EditorSettings::_load_defaults(Ref<ConfigFile> p_extra_config) { EDITOR_SETTING(Variant::FLOAT, PROPERTY_HINT_RANGE, "editors/2d/bone_outline_size", 2.0, "0.01,8,0.01,or_greater") _initial_set("editors/2d/viewport_border_color", Color(0.4, 0.4, 1.0, 0.4)); _initial_set("editors/2d/use_integer_zoom_by_default", false); + EDITOR_SETTING(Variant::FLOAT, PROPERTY_HINT_RANGE, "editors/2d/zoom_speed_factor", 1.1, "1.01,2,0.01") // Panning // Enum should be in sync with ControlScheme in ViewPanner. @@ -824,6 +827,12 @@ void EditorSettings::_load_defaults(Ref<ConfigFile> p_extra_config) { String android_window_hints = "Auto (based on screen size):0,Same as Editor:1,Side-by-side with Editor:2"; EDITOR_SETTING(Variant::INT, PROPERTY_HINT_ENUM, "run/window_placement/android_window", 0, android_window_hints) + int default_play_window_pip_mode = 0; +#ifdef ANDROID_ENABLED + default_play_window_pip_mode = 2; +#endif + EDITOR_SETTING(Variant::INT, PROPERTY_HINT_ENUM, "run/window_placement/play_window_pip_mode", default_play_window_pip_mode, "Disabled:0,Enabled:1,Enabled when Play window is same as Editor:2") + // Auto save _initial_set("run/auto_save/save_before_running", true); diff --git a/editor/editor_settings_dialog.cpp b/editor/editor_settings_dialog.cpp index a71d43ad51..7e4dec86e9 100644 --- a/editor/editor_settings_dialog.cpp +++ b/editor/editor_settings_dialog.cpp @@ -42,6 +42,7 @@ #include "editor/editor_undo_redo_manager.h" #include "editor/event_listener_line_edit.h" #include "editor/input_event_configuration_dialog.h" +#include "editor/plugins/node_3d_editor_plugin.h" #include "editor/themes/editor_scale.h" #include "editor/themes/editor_theme_manager.h" #include "scene/gui/panel_container.h" @@ -74,7 +75,82 @@ void EditorSettingsDialog::_settings_property_edited(const String &p_name) { EditorSettings::get_singleton()->set_manually("text_editor/theme/color_theme", "Custom"); } else if (full_name.begins_with("editors/visual_editors/connection_colors") || full_name.begins_with("editors/visual_editors/category_colors")) { EditorSettings::get_singleton()->set_manually("editors/visual_editors/color_theme", "Custom"); - } + } else if (full_name == "editors/3d/navigation/orbit_mouse_button" || full_name == "editors/3d/navigation/pan_mouse_button" || full_name == "editors/3d/navigation/zoom_mouse_button") { + EditorSettings::get_singleton()->set_manually("editors/3d/navigation/navigation_scheme", (int)Node3DEditorViewport::NAVIGATION_CUSTOM); + } else if (full_name == "editors/3d/navigation/navigation_scheme") { + update_navigation_preset(); + } +} + +void EditorSettingsDialog::update_navigation_preset() { + Node3DEditorViewport::NavigationScheme nav_scheme = (Node3DEditorViewport::NavigationScheme)EDITOR_GET("editors/3d/navigation/navigation_scheme").operator int(); + Node3DEditorViewport::ViewportNavMouseButton set_orbit_mouse_button = Node3DEditorViewport::NAVIGATION_LEFT_MOUSE; + Node3DEditorViewport::ViewportNavMouseButton set_pan_mouse_button = Node3DEditorViewport::NAVIGATION_LEFT_MOUSE; + Node3DEditorViewport::ViewportNavMouseButton set_zoom_mouse_button = Node3DEditorViewport::NAVIGATION_LEFT_MOUSE; + Ref<InputEventKey> orbit_mod_key_1; + Ref<InputEventKey> orbit_mod_key_2; + Ref<InputEventKey> pan_mod_key_1; + Ref<InputEventKey> pan_mod_key_2; + Ref<InputEventKey> zoom_mod_key_1; + Ref<InputEventKey> zoom_mod_key_2; + bool set_preset = false; + + if (nav_scheme == Node3DEditorViewport::NAVIGATION_GODOT) { + set_preset = true; + set_orbit_mouse_button = Node3DEditorViewport::NAVIGATION_MIDDLE_MOUSE; + set_pan_mouse_button = Node3DEditorViewport::NAVIGATION_MIDDLE_MOUSE; + set_zoom_mouse_button = Node3DEditorViewport::NAVIGATION_MIDDLE_MOUSE; + orbit_mod_key_1 = InputEventKey::create_reference(Key::NONE); + orbit_mod_key_2 = InputEventKey::create_reference(Key::NONE); + pan_mod_key_1 = InputEventKey::create_reference(Key::SHIFT); + pan_mod_key_2 = InputEventKey::create_reference(Key::NONE); + zoom_mod_key_1 = InputEventKey::create_reference(Key::SHIFT); + zoom_mod_key_2 = InputEventKey::create_reference(Key::CTRL); + } else if (nav_scheme == Node3DEditorViewport::NAVIGATION_MAYA) { + set_preset = true; + set_orbit_mouse_button = Node3DEditorViewport::NAVIGATION_LEFT_MOUSE; + set_pan_mouse_button = Node3DEditorViewport::NAVIGATION_MIDDLE_MOUSE; + set_zoom_mouse_button = Node3DEditorViewport::NAVIGATION_RIGHT_MOUSE; + orbit_mod_key_1 = InputEventKey::create_reference(Key::ALT); + orbit_mod_key_2 = InputEventKey::create_reference(Key::NONE); + pan_mod_key_1 = InputEventKey::create_reference(Key::NONE); + pan_mod_key_2 = InputEventKey::create_reference(Key::NONE); + zoom_mod_key_1 = InputEventKey::create_reference(Key::ALT); + zoom_mod_key_2 = InputEventKey::create_reference(Key::NONE); + } else if (nav_scheme == Node3DEditorViewport::NAVIGATION_MODO) { + set_preset = true; + set_orbit_mouse_button = Node3DEditorViewport::NAVIGATION_LEFT_MOUSE; + set_pan_mouse_button = Node3DEditorViewport::NAVIGATION_LEFT_MOUSE; + set_zoom_mouse_button = Node3DEditorViewport::NAVIGATION_LEFT_MOUSE; + orbit_mod_key_1 = InputEventKey::create_reference(Key::ALT); + orbit_mod_key_2 = InputEventKey::create_reference(Key::NONE); + pan_mod_key_1 = InputEventKey::create_reference(Key::SHIFT); + pan_mod_key_2 = InputEventKey::create_reference(Key::ALT); + zoom_mod_key_1 = InputEventKey::create_reference(Key::ALT); + zoom_mod_key_2 = InputEventKey::create_reference(Key::CTRL); + } + // Set settings to the desired preset values. + if (set_preset) { + EditorSettings::get_singleton()->set_manually("editors/3d/navigation/orbit_mouse_button", (int)set_orbit_mouse_button); + EditorSettings::get_singleton()->set_manually("editors/3d/navigation/pan_mouse_button", (int)set_pan_mouse_button); + EditorSettings::get_singleton()->set_manually("editors/3d/navigation/zoom_mouse_button", (int)set_zoom_mouse_button); + _set_shortcut_input("spatial_editor/viewport_orbit_modifier_1", orbit_mod_key_1); + _set_shortcut_input("spatial_editor/viewport_orbit_modifier_2", orbit_mod_key_2); + _set_shortcut_input("spatial_editor/viewport_pan_modifier_1", pan_mod_key_1); + _set_shortcut_input("spatial_editor/viewport_pan_modifier_2", pan_mod_key_2); + _set_shortcut_input("spatial_editor/viewport_zoom_modifier_1", zoom_mod_key_1); + _set_shortcut_input("spatial_editor/viewport_zoom_modifier_2", zoom_mod_key_2); + } +} + +void EditorSettingsDialog::_set_shortcut_input(const String &p_name, Ref<InputEventKey> &p_event) { + Array sc_events; + if (p_event->get_keycode() != Key::NONE) { + sc_events.push_back((Variant)p_event); + } + + Ref<Shortcut> sc = EditorSettings::get_singleton()->get_shortcut(p_name); + sc->set_events(sc_events); } void EditorSettingsDialog::_settings_save() { @@ -97,6 +173,8 @@ void EditorSettingsDialog::popup_edit_settings() { EditorSettings::get_singleton()->list_text_editor_themes(); // make sure we have an up to date list of themes + _update_dynamic_property_hints(); + inspector->edit(EditorSettings::get_singleton()); inspector->get_inspector()->update_tree(); @@ -139,8 +217,8 @@ void EditorSettingsDialog::_notification(int p_what) { case NOTIFICATION_READY: { EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); - undo_redo->get_or_create_history(EditorUndoRedoManager::GLOBAL_HISTORY).undo_redo->set_method_notify_callback(EditorDebuggerNode::_method_changeds, nullptr); - undo_redo->get_or_create_history(EditorUndoRedoManager::GLOBAL_HISTORY).undo_redo->set_property_notify_callback(EditorDebuggerNode::_property_changeds, nullptr); + undo_redo->get_or_create_history(EditorUndoRedoManager::GLOBAL_HISTORY).undo_redo->set_method_notify_callback(EditorDebuggerNode::_methods_changed, nullptr); + undo_redo->get_or_create_history(EditorUndoRedoManager::GLOBAL_HISTORY).undo_redo->set_property_notify_callback(EditorDebuggerNode::_properties_changed, nullptr); undo_redo->get_or_create_history(EditorUndoRedoManager::GLOBAL_HISTORY).undo_redo->set_commit_notify_callback(_undo_redo_callback, this); } break; @@ -160,6 +238,12 @@ void EditorSettingsDialog::_notification(int p_what) { _update_shortcuts(); } + if (EditorSettings::get_singleton()->check_changed_settings_in_group("editors/3d/navigation")) { + // Shortcuts may have changed, so dynamic hint values must update. + _update_dynamic_property_hints(); + inspector->get_inspector()->update_tree(); + } + if (EditorSettings::get_singleton()->check_changed_settings_in_group("interface/editor/localize_settings")) { inspector->update_category_list(); } @@ -256,6 +340,13 @@ void EditorSettingsDialog::_update_shortcut_events(const String &p_path, const A undo_redo->add_do_method(this, "_settings_changed"); undo_redo->add_undo_method(this, "_settings_changed"); undo_redo->commit_action(); + + bool path_is_orbit_mod = p_path == "spatial_editor/viewport_orbit_modifier_1" || p_path == "spatial_editor/viewport_orbit_modifier_2"; + bool path_is_pan_mod = p_path == "spatial_editor/viewport_pan_modifier_1" || p_path == "spatial_editor/viewport_pan_modifier_2"; + bool path_is_zoom_mod = p_path == "spatial_editor/viewport_zoom_modifier_1" || p_path == "spatial_editor/viewport_zoom_modifier_2"; + if (path_is_orbit_mod || path_is_pan_mod || path_is_zoom_mod) { + EditorSettings::get_singleton()->set_manually("editors/3d/navigation/navigation_scheme", (int)Node3DEditorViewport::NAVIGATION_CUSTOM); + } } Array EditorSettingsDialog::_event_list_to_array_helper(const List<Ref<InputEvent>> &p_events) { @@ -661,6 +752,40 @@ void EditorSettingsDialog::drop_data_fw(const Point2 &p_point, const Variant &p_ void EditorSettingsDialog::_tabs_tab_changed(int p_tab) { _focus_current_search_box(); + + // When tab has switched, shortcuts may have changed. + _update_dynamic_property_hints(); + inspector->get_inspector()->update_tree(); +} + +void EditorSettingsDialog::_update_dynamic_property_hints() { + // Calling add_property_hint overrides the existing hint. + EditorSettings *settings = EditorSettings::get_singleton(); + settings->add_property_hint(_create_mouse_shortcut_property_info("editors/3d/navigation/orbit_mouse_button", "spatial_editor/viewport_orbit_modifier_1", "spatial_editor/viewport_orbit_modifier_2")); + settings->add_property_hint(_create_mouse_shortcut_property_info("editors/3d/navigation/pan_mouse_button", "spatial_editor/viewport_pan_modifier_1", "spatial_editor/viewport_pan_modifier_2")); + settings->add_property_hint(_create_mouse_shortcut_property_info("editors/3d/navigation/zoom_mouse_button", "spatial_editor/viewport_zoom_modifier_1", "spatial_editor/viewport_zoom_modifier_2")); +} + +PropertyInfo EditorSettingsDialog::_create_mouse_shortcut_property_info(const String &p_property_name, const String &p_shortcut_1_name, const String &p_shortcut_2_name) { + String hint_string; + hint_string += _get_shortcut_button_string(p_shortcut_1_name) + _get_shortcut_button_string(p_shortcut_2_name); + hint_string += "Left Mouse,"; + hint_string += _get_shortcut_button_string(p_shortcut_1_name) + _get_shortcut_button_string(p_shortcut_2_name); + hint_string += "Middle Mouse,"; + hint_string += _get_shortcut_button_string(p_shortcut_1_name) + _get_shortcut_button_string(p_shortcut_2_name); + hint_string += "Right Mouse"; + + return PropertyInfo(Variant::INT, p_property_name, PROPERTY_HINT_ENUM, hint_string); +} + +String EditorSettingsDialog::_get_shortcut_button_string(const String &p_shortcut_name) { + String button_string; + Ref<Shortcut> shortcut_ref = EditorSettings::get_singleton()->get_shortcut(p_shortcut_name); + Array events = shortcut_ref->get_events(); + for (Ref<InputEvent> input_event : events) { + button_string += input_event->as_text() + " + "; + } + return button_string; } void EditorSettingsDialog::_focus_current_search_box() { diff --git a/editor/editor_settings_dialog.h b/editor/editor_settings_dialog.h index cab8fe9da1..c213f737e2 100644 --- a/editor/editor_settings_dialog.h +++ b/editor/editor_settings_dialog.h @@ -100,6 +100,10 @@ class EditorSettingsDialog : public AcceptDialog { void _tabs_tab_changed(int p_tab); void _focus_current_search_box(); + void _update_dynamic_property_hints(); + PropertyInfo _create_mouse_shortcut_property_info(const String &p_property_name, const String &p_shortcut_1_name, const String &p_shortcut_2_name); + String _get_shortcut_button_string(const String &p_shortcut_name); + void _filter_shortcuts(const String &p_filter); void _filter_shortcuts_by_event(const Ref<InputEvent> &p_event); bool _should_display_shortcut(const String &p_name, const Array &p_events) const; @@ -107,6 +111,7 @@ class EditorSettingsDialog : public AcceptDialog { void _update_shortcuts(); void _shortcut_button_pressed(Object *p_item, int p_column, int p_idx, MouseButton p_button = MouseButton::LEFT); void _shortcut_cell_double_clicked(); + static void _set_shortcut_input(const String &p_name, Ref<InputEventKey> &p_event); static void _undo_redo_callback(void *p_self, const String &p_name); @@ -124,6 +129,7 @@ protected: public: void popup_edit_settings(); + static void update_navigation_preset(); EditorSettingsDialog(); ~EditorSettingsDialog(); diff --git a/editor/editor_undo_redo_manager.cpp b/editor/editor_undo_redo_manager.cpp index 55bc198dfb..c0bf216634 100644 --- a/editor/editor_undo_redo_manager.cpp +++ b/editor/editor_undo_redo_manager.cpp @@ -390,7 +390,7 @@ bool EditorUndoRedoManager::has_history(int p_idx) const { return history_map.has(p_idx); } -void EditorUndoRedoManager::clear_history(bool p_increase_version, int p_idx) { +void EditorUndoRedoManager::clear_history(int p_idx, bool p_increase_version) { if (p_idx != INVALID_HISTORY) { History &history = get_or_create_history(p_idx); history.undo_redo->clear_history(p_increase_version); @@ -507,6 +507,7 @@ void EditorUndoRedoManager::_bind_methods() { ClassDB::bind_method(D_METHOD("get_object_history_id", "object"), &EditorUndoRedoManager::get_history_id_for_object); ClassDB::bind_method(D_METHOD("get_history_undo_redo", "id"), &EditorUndoRedoManager::get_history_undo_redo); + ClassDB::bind_method(D_METHOD("clear_history", "id", "increase_version"), &EditorUndoRedoManager::clear_history, DEFVAL(INVALID_HISTORY), DEFVAL(true)); ADD_SIGNAL(MethodInfo("history_changed")); ADD_SIGNAL(MethodInfo("version_changed")); diff --git a/editor/editor_undo_redo_manager.h b/editor/editor_undo_redo_manager.h index 219d5e0702..54475c3c6c 100644 --- a/editor/editor_undo_redo_manager.h +++ b/editor/editor_undo_redo_manager.h @@ -125,7 +125,7 @@ public: bool undo_history(int p_id); bool redo(); bool redo_history(int p_id); - void clear_history(bool p_increase_version = true, int p_idx = INVALID_HISTORY); + void clear_history(int p_idx = INVALID_HISTORY, bool p_increase_version = true); void set_history_as_saved(int p_idx); void set_history_as_unsaved(int p_idx); diff --git a/editor/export/editor_export.cpp b/editor/export/editor_export.cpp index 72ab186036..975a601ae1 100644 --- a/editor/export/editor_export.cpp +++ b/editor/export/editor_export.cpp @@ -124,7 +124,17 @@ void EditorExport::_bind_methods() { void EditorExport::add_export_platform(const Ref<EditorExportPlatform> &p_platform) { export_platforms.push_back(p_platform); + should_update_presets = true; + should_reload_presets = true; +} + +void EditorExport::remove_export_platform(const Ref<EditorExportPlatform> &p_platform) { + export_platforms.erase(p_platform); + p_platform->cleanup(); + + should_update_presets = true; + should_reload_presets = true; } int EditorExport::get_export_platform_count() { @@ -244,7 +254,7 @@ void EditorExport::load_config() { if (!preset.is_valid()) { index++; - ERR_CONTINUE(!preset.is_valid()); + continue; // Unknown platform, skip without error (platform might be loaded later). } preset->set_name(config->get_value(section, "name")); @@ -343,6 +353,12 @@ void EditorExport::load_config() { void EditorExport::update_export_presets() { HashMap<StringName, List<EditorExportPlatform::ExportOption>> platform_options; + if (should_reload_presets) { + should_reload_presets = false; + export_presets.clear(); + load_config(); + } + for (int i = 0; i < export_platforms.size(); i++) { Ref<EditorExportPlatform> platform = export_platforms[i]; diff --git a/editor/export/editor_export.h b/editor/export/editor_export.h index f8cb90dc39..ebb2038f53 100644 --- a/editor/export/editor_export.h +++ b/editor/export/editor_export.h @@ -47,6 +47,7 @@ class EditorExport : public Node { Timer *save_timer = nullptr; bool block_save = false; bool should_update_presets = false; + bool should_reload_presets = false; static EditorExport *singleton; @@ -66,6 +67,7 @@ public: void add_export_platform(const Ref<EditorExportPlatform> &p_platform); int get_export_platform_count(); Ref<EditorExportPlatform> get_export_platform(int p_idx); + void remove_export_platform(const Ref<EditorExportPlatform> &p_platform); void add_export_preset(const Ref<EditorExportPreset> &p_preset, int p_at_pos = -1); int get_export_preset_count() const; diff --git a/editor/export/editor_export_platform.cpp b/editor/export/editor_export_platform.cpp index 0768ae128b..7ad589a58d 100644 --- a/editor/export/editor_export_platform.cpp +++ b/editor/export/editor_export_platform.cpp @@ -71,7 +71,7 @@ bool EditorExportPlatform::fill_log_messages(RichTextLabel *p_log, Error p_err) p_log->add_text(" "); p_log->add_text(get_name()); p_log->add_text(" - "); - if (p_err == OK) { + if (p_err == OK && get_worst_message_type() < EditorExportPlatform::EXPORT_MESSAGE_ERROR) { if (get_worst_message_type() >= EditorExportPlatform::EXPORT_MESSAGE_WARNING) { p_log->add_image(p_log->get_editor_theme_icon(SNAME("StatusWarning")), 16 * EDSCALE, 16 * EDSCALE, Color(1.0, 1.0, 1.0), INLINE_ALIGNMENT_CENTER); p_log->add_text(" "); @@ -167,58 +167,6 @@ bool EditorExportPlatform::fill_log_messages(RichTextLabel *p_log, Error p_err) return has_messages; } -void EditorExportPlatform::gen_debug_flags(Vector<String> &r_flags, int p_flags) { - String host = EDITOR_GET("network/debug/remote_host"); - int remote_port = (int)EDITOR_GET("network/debug/remote_port"); - - if (EditorSettings::get_singleton()->has_setting("export/android/use_wifi_for_remote_debug") && EDITOR_GET("export/android/use_wifi_for_remote_debug")) { - host = EDITOR_GET("export/android/wifi_remote_debug_host"); - } else if (p_flags & DEBUG_FLAG_REMOTE_DEBUG_LOCALHOST) { - host = "localhost"; - } - - if (p_flags & DEBUG_FLAG_DUMB_CLIENT) { - int port = EDITOR_GET("filesystem/file_server/port"); - String passwd = EDITOR_GET("filesystem/file_server/password"); - r_flags.push_back("--remote-fs"); - r_flags.push_back(host + ":" + itos(port)); - if (!passwd.is_empty()) { - r_flags.push_back("--remote-fs-password"); - r_flags.push_back(passwd); - } - } - - if (p_flags & DEBUG_FLAG_REMOTE_DEBUG) { - r_flags.push_back("--remote-debug"); - - r_flags.push_back(get_debug_protocol() + host + ":" + String::num(remote_port)); - - List<String> breakpoints; - ScriptEditor::get_singleton()->get_breakpoints(&breakpoints); - - if (breakpoints.size()) { - r_flags.push_back("--breakpoints"); - String bpoints; - for (const List<String>::Element *E = breakpoints.front(); E; E = E->next()) { - bpoints += E->get().replace(" ", "%20"); - if (E->next()) { - bpoints += ","; - } - } - - r_flags.push_back(bpoints); - } - } - - if (p_flags & DEBUG_FLAG_VIEW_COLLISIONS) { - r_flags.push_back("--debug-collisions"); - } - - if (p_flags & DEBUG_FLAG_VIEW_NAVIGATION) { - r_flags.push_back("--debug-navigation"); - } -} - Error EditorExportPlatform::_save_pack_file(void *p_userdata, const String &p_path, const Vector<uint8_t> &p_data, int p_file, int p_total, const Vector<String> &p_enc_in_filters, const Vector<String> &p_enc_ex_filters, const Vector<uint8_t> &p_key) { ERR_FAIL_COND_V_MSG(p_total < 1, ERR_PARAMETER_RANGE_ERROR, "Must select at least one file to export."); @@ -530,7 +478,7 @@ HashSet<String> EditorExportPlatform::get_features(const Ref<EditorExportPreset> return result; } -EditorExportPlatform::ExportNotifier::ExportNotifier(EditorExportPlatform &p_platform, const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags) { +EditorExportPlatform::ExportNotifier::ExportNotifier(EditorExportPlatform &p_platform, const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, BitField<EditorExportPlatform::DebugFlags> p_flags) { HashSet<String> features = p_platform.get_features(p_preset, p_debug); Vector<Ref<EditorExportPlugin>> export_plugins = EditorExport::get_singleton()->get_export_plugins(); //initial export plugin callback @@ -919,6 +867,55 @@ Vector<String> EditorExportPlatform::get_forced_export_files() { return files; } +Error EditorExportPlatform::_script_save_file(void *p_userdata, const String &p_path, const Vector<uint8_t> &p_data, int p_file, int p_total, const Vector<String> &p_enc_in_filters, const Vector<String> &p_enc_ex_filters, const Vector<uint8_t> &p_key) { + Callable cb = ((ScriptCallbackData *)p_userdata)->file_cb; + ERR_FAIL_COND_V(!cb.is_valid(), FAILED); + + Variant path = p_path; + Variant data = p_data; + Variant file = p_file; + Variant total = p_total; + Variant enc_in = p_enc_in_filters; + Variant enc_ex = p_enc_ex_filters; + Variant enc_key = p_key; + + Variant ret; + Callable::CallError ce; + const Variant *args[7] = { &path, &data, &file, &total, &enc_in, &enc_ex, &enc_key }; + + cb.callp(args, 7, ret, ce); + ERR_FAIL_COND_V_MSG(ce.error != Callable::CallError::CALL_OK, FAILED, vformat("Failed to execute file save callback: %s.", Variant::get_callable_error_text(cb, args, 7, ce))); + + return (Error)ret.operator int(); +} + +Error EditorExportPlatform::_script_add_shared_object(void *p_userdata, const SharedObject &p_so) { + Callable cb = ((ScriptCallbackData *)p_userdata)->so_cb; + if (!cb.is_valid()) { + return OK; // Optional. + } + + Variant path = p_so.path; + Variant tags = p_so.tags; + Variant target = p_so.target; + + Variant ret; + Callable::CallError ce; + const Variant *args[3] = { &path, &tags, &target }; + + cb.callp(args, 3, ret, ce); + ERR_FAIL_COND_V_MSG(ce.error != Callable::CallError::CALL_OK, FAILED, vformat("Failed to execute shared object save callback: %s.", Variant::get_callable_error_text(cb, args, 3, ce))); + + return (Error)ret.operator int(); +} + +Error EditorExportPlatform::_export_project_files(const Ref<EditorExportPreset> &p_preset, bool p_debug, const Callable &p_save_func, const Callable &p_so_func) { + ScriptCallbackData data; + data.file_cb = p_save_func; + data.so_cb = p_so_func; + return export_project_files(p_preset, p_debug, _script_save_file, &data, _script_add_shared_object); +} + Error EditorExportPlatform::export_project_files(const Ref<EditorExportPreset> &p_preset, bool p_debug, EditorExportSaveFunction p_func, void *p_udata, EditorExportSaveSharedObject p_so_func) { //figure out paths of files that will be exported HashSet<String> paths; @@ -1425,7 +1422,7 @@ Error EditorExportPlatform::export_project_files(const Ref<EditorExportPreset> & return p_func(p_udata, "res://" + config_file, data, idx, total, enc_in_filters, enc_ex_filters, key); } -Error EditorExportPlatform::_add_shared_object(void *p_userdata, const SharedObject &p_so) { +Error EditorExportPlatform::_pack_add_shared_object(void *p_userdata, const SharedObject &p_so) { PackData *pack_data = (PackData *)p_userdata; if (pack_data->so_files) { pack_data->so_files->push_back(p_so); @@ -1434,6 +1431,15 @@ Error EditorExportPlatform::_add_shared_object(void *p_userdata, const SharedObj return OK; } +Error EditorExportPlatform::_zip_add_shared_object(void *p_userdata, const SharedObject &p_so) { + ZipData *zip_data = (ZipData *)p_userdata; + if (zip_data->so_files) { + zip_data->so_files->push_back(p_so); + } + + return OK; +} + void EditorExportPlatform::zip_folder_recursive(zipFile &p_zip, const String &p_root_path, const String &p_folder, const String &p_pkg_name) { String dir = p_folder.is_empty() ? p_root_path : p_root_path.path_join(p_folder); @@ -1551,6 +1557,54 @@ void EditorExportPlatform::zip_folder_recursive(zipFile &p_zip, const String &p_ da->list_dir_end(); } +Dictionary EditorExportPlatform::_save_pack(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, bool p_embed) { + Vector<SharedObject> so_files; + int64_t embedded_start = 0; + int64_t embedded_size = 0; + Error err_code = save_pack(p_preset, p_debug, p_path, &so_files, p_embed, &embedded_start, &embedded_size); + + Dictionary ret; + ret["result"] = err_code; + if (err_code == OK) { + Array arr; + for (const SharedObject &E : so_files) { + Dictionary so; + so["path"] = E.path; + so["tags"] = E.tags; + so["target_folder"] = E.target; + arr.push_back(so); + } + ret["so_files"] = arr; + if (p_embed) { + ret["embedded_start"] = embedded_start; + ret["embedded_size"] = embedded_size; + } + } + + return ret; +} + +Dictionary EditorExportPlatform::_save_zip(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path) { + Vector<SharedObject> so_files; + Error err_code = save_zip(p_preset, p_debug, p_path, &so_files); + + Dictionary ret; + ret["result"] = err_code; + if (err_code == OK) { + Array arr; + for (const SharedObject &E : so_files) { + Dictionary so; + so["path"] = E.path; + so["tags"] = E.tags; + so["target_folder"] = E.target; + arr.push_back(so); + } + ret["so_files"] = arr; + } + + return ret; +} + Error EditorExportPlatform::save_pack(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, Vector<SharedObject> *p_so_files, bool p_embed, int64_t *r_embedded_start, int64_t *r_embedded_size) { EditorProgress ep("savepack", TTR("Packing"), 102, true); @@ -1570,7 +1624,7 @@ Error EditorExportPlatform::save_pack(const Ref<EditorExportPreset> &p_preset, b pd.f = ftmp; pd.so_files = p_so_files; - Error err = export_project_files(p_preset, p_debug, _save_pack_file, &pd, _add_shared_object); + Error err = export_project_files(p_preset, p_debug, _save_pack_file, &pd, _pack_add_shared_object); // Close temp file. pd.f.unref(); @@ -1777,7 +1831,7 @@ Error EditorExportPlatform::save_pack(const Ref<EditorExportPreset> &p_preset, b return OK; } -Error EditorExportPlatform::save_zip(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path) { +Error EditorExportPlatform::save_zip(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, Vector<SharedObject> *p_so_files) { EditorProgress ep("savezip", TTR("Packing"), 102, true); Ref<FileAccess> io_fa; @@ -1787,8 +1841,9 @@ Error EditorExportPlatform::save_zip(const Ref<EditorExportPreset> &p_preset, bo ZipData zd; zd.ep = &ep; zd.zip = zip; + zd.so_files = p_so_files; - Error err = export_project_files(p_preset, p_debug, _save_zip_file, &zd); + Error err = export_project_files(p_preset, p_debug, _save_zip_file, &zd, _zip_add_shared_object); if (err != OK && err != ERR_SKIP) { add_message(EXPORT_MESSAGE_ERROR, TTR("Save ZIP"), TTR("Failed to export project files.")); } @@ -1798,45 +1853,48 @@ Error EditorExportPlatform::save_zip(const Ref<EditorExportPreset> &p_preset, bo return OK; } -Error EditorExportPlatform::export_pack(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags) { +Error EditorExportPlatform::export_pack(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, BitField<EditorExportPlatform::DebugFlags> p_flags) { ExportNotifier notifier(*this, p_preset, p_debug, p_path, p_flags); return save_pack(p_preset, p_debug, p_path); } -Error EditorExportPlatform::export_zip(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags) { +Error EditorExportPlatform::export_zip(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, BitField<EditorExportPlatform::DebugFlags> p_flags) { ExportNotifier notifier(*this, p_preset, p_debug, p_path, p_flags); return save_zip(p_preset, p_debug, p_path); } -void EditorExportPlatform::gen_export_flags(Vector<String> &r_flags, int p_flags) { +Vector<String> EditorExportPlatform::gen_export_flags(BitField<EditorExportPlatform::DebugFlags> p_flags) { + Vector<String> ret; String host = EDITOR_GET("network/debug/remote_host"); int remote_port = (int)EDITOR_GET("network/debug/remote_port"); - if (p_flags & DEBUG_FLAG_REMOTE_DEBUG_LOCALHOST) { + if (get_name() == "Android" && EditorSettings::get_singleton()->has_setting("export/android/use_wifi_for_remote_debug") && EDITOR_GET("export/android/use_wifi_for_remote_debug")) { + host = EDITOR_GET("export/android/wifi_remote_debug_host"); + } else if (p_flags.has_flag(DEBUG_FLAG_REMOTE_DEBUG_LOCALHOST)) { host = "localhost"; } - if (p_flags & DEBUG_FLAG_DUMB_CLIENT) { + if (p_flags.has_flag(DEBUG_FLAG_DUMB_CLIENT)) { int port = EDITOR_GET("filesystem/file_server/port"); String passwd = EDITOR_GET("filesystem/file_server/password"); - r_flags.push_back("--remote-fs"); - r_flags.push_back(host + ":" + itos(port)); + ret.push_back("--remote-fs"); + ret.push_back(host + ":" + itos(port)); if (!passwd.is_empty()) { - r_flags.push_back("--remote-fs-password"); - r_flags.push_back(passwd); + ret.push_back("--remote-fs-password"); + ret.push_back(passwd); } } - if (p_flags & DEBUG_FLAG_REMOTE_DEBUG) { - r_flags.push_back("--remote-debug"); + if (p_flags.has_flag(DEBUG_FLAG_REMOTE_DEBUG)) { + ret.push_back("--remote-debug"); - r_flags.push_back(get_debug_protocol() + host + ":" + String::num(remote_port)); + ret.push_back(get_debug_protocol() + host + ":" + String::num(remote_port)); List<String> breakpoints; ScriptEditor::get_singleton()->get_breakpoints(&breakpoints); if (breakpoints.size()) { - r_flags.push_back("--breakpoints"); + ret.push_back("--breakpoints"); String bpoints; for (List<String>::Element *E = breakpoints.front(); E; E = E->next()) { bpoints += E->get().replace(" ", "%20"); @@ -1845,23 +1903,23 @@ void EditorExportPlatform::gen_export_flags(Vector<String> &r_flags, int p_flags } } - r_flags.push_back(bpoints); + ret.push_back(bpoints); } } - if (p_flags & DEBUG_FLAG_VIEW_COLLISIONS) { - r_flags.push_back("--debug-collisions"); + if (p_flags.has_flag(DEBUG_FLAG_VIEW_COLLISIONS)) { + ret.push_back("--debug-collisions"); } - if (p_flags & DEBUG_FLAG_VIEW_NAVIGATION) { - r_flags.push_back("--debug-navigation"); + if (p_flags.has_flag(DEBUG_FLAG_VIEW_NAVIGATION)) { + ret.push_back("--debug-navigation"); } + return ret; } bool EditorExportPlatform::can_export(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates, bool p_debug) const { bool valid = true; -#ifndef ANDROID_ENABLED String templates_error; valid = valid && has_valid_export_configuration(p_preset, templates_error, r_missing_templates, p_debug); @@ -1886,7 +1944,6 @@ bool EditorExportPlatform::can_export(const Ref<EditorExportPreset> &p_preset, S if (!export_plugins_warning.is_empty()) { r_error += export_plugins_warning; } -#endif String project_configuration_error; valid = valid && has_valid_project_configuration(p_preset, project_configuration_error); @@ -2037,8 +2094,61 @@ Error EditorExportPlatform::ssh_push_to_remote(const String &p_host, const Strin return OK; } +Array EditorExportPlatform::get_current_presets() const { + Array ret; + for (int i = 0; i < EditorExport::get_singleton()->get_export_preset_count(); i++) { + Ref<EditorExportPreset> ep = EditorExport::get_singleton()->get_export_preset(i); + if (ep->get_platform() == this) { + ret.push_back(ep); + } + } + return ret; +} + void EditorExportPlatform::_bind_methods() { ClassDB::bind_method(D_METHOD("get_os_name"), &EditorExportPlatform::get_os_name); + + ClassDB::bind_method(D_METHOD("create_preset"), &EditorExportPlatform::create_preset); + + ClassDB::bind_method(D_METHOD("find_export_template", "template_file_name"), &EditorExportPlatform::_find_export_template); + ClassDB::bind_method(D_METHOD("get_current_presets"), &EditorExportPlatform::get_current_presets); + + ClassDB::bind_method(D_METHOD("save_pack", "preset", "debug", "path", "embed"), &EditorExportPlatform::_save_pack, DEFVAL(false)); + ClassDB::bind_method(D_METHOD("save_zip", "preset", "debug", "path"), &EditorExportPlatform::_save_zip); + + ClassDB::bind_method(D_METHOD("gen_export_flags", "flags"), &EditorExportPlatform::gen_export_flags); + + ClassDB::bind_method(D_METHOD("export_project_files", "preset", "debug", "save_cb", "shared_cb"), &EditorExportPlatform::_export_project_files, DEFVAL(Callable())); + + ClassDB::bind_method(D_METHOD("export_project", "preset", "debug", "path", "flags"), &EditorExportPlatform::export_project, DEFVAL(0)); + ClassDB::bind_method(D_METHOD("export_pack", "preset", "debug", "path", "flags"), &EditorExportPlatform::export_pack, DEFVAL(0)); + ClassDB::bind_method(D_METHOD("export_zip", "preset", "debug", "path", "flags"), &EditorExportPlatform::export_zip, DEFVAL(0)); + + ClassDB::bind_method(D_METHOD("clear_messages"), &EditorExportPlatform::clear_messages); + ClassDB::bind_method(D_METHOD("add_message", "type", "category", "message"), &EditorExportPlatform::add_message); + ClassDB::bind_method(D_METHOD("get_message_count"), &EditorExportPlatform::get_message_count); + + ClassDB::bind_method(D_METHOD("get_message_type", "index"), &EditorExportPlatform::_get_message_type); + ClassDB::bind_method(D_METHOD("get_message_category", "index"), &EditorExportPlatform::_get_message_category); + ClassDB::bind_method(D_METHOD("get_message_text", "index"), &EditorExportPlatform::_get_message_text); + ClassDB::bind_method(D_METHOD("get_worst_message_type"), &EditorExportPlatform::get_worst_message_type); + + ClassDB::bind_method(D_METHOD("ssh_run_on_remote", "host", "port", "ssh_arg", "cmd_args", "output", "port_fwd"), &EditorExportPlatform::_ssh_run_on_remote, DEFVAL(Array()), DEFVAL(-1)); + ClassDB::bind_method(D_METHOD("ssh_run_on_remote_no_wait", "host", "port", "ssh_args", "cmd_args", "port_fwd"), &EditorExportPlatform::_ssh_run_on_remote_no_wait, DEFVAL(-1)); + ClassDB::bind_method(D_METHOD("ssh_push_to_remote", "host", "port", "scp_args", "src_file", "dst_file"), &EditorExportPlatform::ssh_push_to_remote); + + ClassDB::bind_static_method("EditorExportPlatform", D_METHOD("get_forced_export_files"), &EditorExportPlatform::get_forced_export_files); + + BIND_ENUM_CONSTANT(EXPORT_MESSAGE_NONE); + BIND_ENUM_CONSTANT(EXPORT_MESSAGE_INFO); + BIND_ENUM_CONSTANT(EXPORT_MESSAGE_WARNING); + BIND_ENUM_CONSTANT(EXPORT_MESSAGE_ERROR); + + BIND_BITFIELD_FLAG(DEBUG_FLAG_DUMB_CLIENT); + BIND_BITFIELD_FLAG(DEBUG_FLAG_REMOTE_DEBUG); + BIND_BITFIELD_FLAG(DEBUG_FLAG_REMOTE_DEBUG_LOCALHOST); + BIND_BITFIELD_FLAG(DEBUG_FLAG_VIEW_COLLISIONS); + BIND_BITFIELD_FLAG(DEBUG_FLAG_VIEW_NAVIGATION); } EditorExportPlatform::EditorExportPlatform() { diff --git a/editor/export/editor_export_platform.h b/editor/export/editor_export_platform.h index 3fd75ff67f..a800bb95e6 100644 --- a/editor/export/editor_export_platform.h +++ b/editor/export/editor_export_platform.h @@ -56,6 +56,14 @@ public: typedef Error (*EditorExportSaveFunction)(void *p_userdata, const String &p_path, const Vector<uint8_t> &p_data, int p_file, int p_total, const Vector<String> &p_enc_in_filters, const Vector<String> &p_enc_ex_filters, const Vector<uint8_t> &p_key); typedef Error (*EditorExportSaveSharedObject)(void *p_userdata, const SharedObject &p_so); + enum DebugFlags { + DEBUG_FLAG_DUMB_CLIENT = 1, + DEBUG_FLAG_REMOTE_DEBUG = 2, + DEBUG_FLAG_REMOTE_DEBUG_LOCALHOST = 4, + DEBUG_FLAG_VIEW_COLLISIONS = 8, + DEBUG_FLAG_VIEW_NAVIGATION = 16, + }; + enum ExportMessageType { EXPORT_MESSAGE_NONE, EXPORT_MESSAGE_INFO, @@ -92,6 +100,7 @@ private: struct ZipData { void *zip = nullptr; EditorProgress *ep = nullptr; + Vector<SharedObject> *so_files = nullptr; }; Vector<ExportMessage> messages; @@ -101,13 +110,22 @@ private: void _export_find_dependencies(const String &p_path, HashSet<String> &p_paths); static Error _save_pack_file(void *p_userdata, const String &p_path, const Vector<uint8_t> &p_data, int p_file, int p_total, const Vector<String> &p_enc_in_filters, const Vector<String> &p_enc_ex_filters, const Vector<uint8_t> &p_key); + static Error _pack_add_shared_object(void *p_userdata, const SharedObject &p_so); + static Error _save_zip_file(void *p_userdata, const String &p_path, const Vector<uint8_t> &p_data, int p_file, int p_total, const Vector<String> &p_enc_in_filters, const Vector<String> &p_enc_ex_filters, const Vector<uint8_t> &p_key); + static Error _zip_add_shared_object(void *p_userdata, const SharedObject &p_so); + + struct ScriptCallbackData { + Callable file_cb; + Callable so_cb; + }; + + static Error _script_save_file(void *p_userdata, const String &p_path, const Vector<uint8_t> &p_data, int p_file, int p_total, const Vector<String> &p_enc_in_filters, const Vector<String> &p_enc_ex_filters, const Vector<uint8_t> &p_key); + static Error _script_add_shared_object(void *p_userdata, const SharedObject &p_so); void _edit_files_with_filter(Ref<DirAccess> &da, const Vector<String> &p_filters, HashSet<String> &r_list, bool exclude); void _edit_filter_list(HashSet<String> &r_list, const String &p_filter, bool exclude); - static Error _add_shared_object(void *p_userdata, const SharedObject &p_so); - struct FileExportCache { uint64_t source_modified_time = 0; String source_md5; @@ -126,19 +144,46 @@ private: protected: struct ExportNotifier { - ExportNotifier(EditorExportPlatform &p_platform, const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags); + ExportNotifier(EditorExportPlatform &p_platform, const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, BitField<EditorExportPlatform::DebugFlags> p_flags); ~ExportNotifier(); }; HashSet<String> get_features(const Ref<EditorExportPreset> &p_preset, bool p_debug) const; - bool exists_export_template(const String &template_file_name, String *err) const; - String find_export_template(const String &template_file_name, String *err = nullptr) const; - void gen_export_flags(Vector<String> &r_flags, int p_flags); - void gen_debug_flags(Vector<String> &r_flags, int p_flags); + Dictionary _find_export_template(const String &p_template_file_name) const { + Dictionary ret; + String err; + + String path = find_export_template(p_template_file_name, &err); + ret["result"] = (err.is_empty() && !path.is_empty()) ? OK : FAILED; + ret["path"] = path; + ret["error_string"] = err; + + return ret; + } + + bool exists_export_template(const String &p_template_file_name, String *r_err) const; + String find_export_template(const String &p_template_file_name, String *r_err = nullptr) const; + Vector<String> gen_export_flags(BitField<EditorExportPlatform::DebugFlags> p_flags); virtual void zip_folder_recursive(zipFile &p_zip, const String &p_root_path, const String &p_folder, const String &p_pkg_name); + Error _ssh_run_on_remote(const String &p_host, const String &p_port, const Vector<String> &p_ssh_args, const String &p_cmd_args, Array r_output = Array(), int p_port_fwd = -1) const { + String pipe; + Error err = ssh_run_on_remote(p_host, p_port, p_ssh_args, p_cmd_args, &pipe, p_port_fwd); + r_output.push_back(pipe); + return err; + } + OS::ProcessID _ssh_run_on_remote_no_wait(const String &p_host, const String &p_port, const Vector<String> &p_ssh_args, const String &p_cmd_args, int p_port_fwd = -1) const { + OS::ProcessID pid = 0; + Error err = ssh_run_on_remote_no_wait(p_host, p_port, p_ssh_args, p_cmd_args, &pid, p_port_fwd); + if (err != OK) { + return -1; + } else { + return pid; + } + } + Error ssh_run_on_remote(const String &p_host, const String &p_port, const Vector<String> &p_ssh_args, const String &p_cmd_args, String *r_out = nullptr, int p_port_fwd = -1) const; Error ssh_run_on_remote_no_wait(const String &p_host, const String &p_port, const Vector<String> &p_ssh_args, const String &p_cmd_args, OS::ProcessID *r_pid = nullptr, int p_port_fwd = -1) const; Error ssh_push_to_remote(const String &p_host, const String &p_port, const Vector<String> &p_scp_args, const String &p_src_file, const String &p_dst_file) const; @@ -195,6 +240,21 @@ public: return messages[p_index]; } + virtual ExportMessageType _get_message_type(int p_index) const { + ERR_FAIL_INDEX_V(p_index, messages.size(), EXPORT_MESSAGE_NONE); + return messages[p_index].msg_type; + } + + virtual String _get_message_category(int p_index) const { + ERR_FAIL_INDEX_V(p_index, messages.size(), String()); + return messages[p_index].category; + } + + virtual String _get_message_text(int p_index) const { + ERR_FAIL_INDEX_V(p_index, messages.size(), String()); + return messages[p_index].text; + } + virtual ExportMessageType get_worst_message_type() const { ExportMessageType worst_type = EXPORT_MESSAGE_NONE; for (int i = 0; i < messages.size(); i++) { @@ -216,10 +276,16 @@ public: virtual String get_name() const = 0; virtual Ref<Texture2D> get_logo() const = 0; + Array get_current_presets() const; + + Error _export_project_files(const Ref<EditorExportPreset> &p_preset, bool p_debug, const Callable &p_save_func, const Callable &p_so_func); Error export_project_files(const Ref<EditorExportPreset> &p_preset, bool p_debug, EditorExportSaveFunction p_func, void *p_udata, EditorExportSaveSharedObject p_so_func = nullptr); + Dictionary _save_pack(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, bool p_embed = false); + Dictionary _save_zip(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path); + Error save_pack(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, Vector<SharedObject> *p_so_files = nullptr, bool p_embed = false, int64_t *r_embedded_start = nullptr, int64_t *r_embedded_size = nullptr); - Error save_zip(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path); + Error save_zip(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, Vector<SharedObject> *p_so_files = nullptr); virtual bool poll_export() { return false; } virtual int get_options_count() const { return 0; } @@ -229,31 +295,26 @@ public: virtual String get_option_tooltip(int p_device) const { return ""; } virtual String get_device_architecture(int p_device) const { return ""; } - enum DebugFlags { - DEBUG_FLAG_DUMB_CLIENT = 1, - DEBUG_FLAG_REMOTE_DEBUG = 2, - DEBUG_FLAG_REMOTE_DEBUG_LOCALHOST = 4, - DEBUG_FLAG_VIEW_COLLISIONS = 8, - DEBUG_FLAG_VIEW_NAVIGATION = 16, - }; - virtual void cleanup() {} - virtual Error run(const Ref<EditorExportPreset> &p_preset, int p_device, int p_debug_flags) { return OK; } + virtual Error run(const Ref<EditorExportPreset> &p_preset, int p_device, BitField<EditorExportPlatform::DebugFlags> p_debug_flags) { return OK; } virtual Ref<Texture2D> get_run_icon() const { return get_logo(); } - bool can_export(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates, bool p_debug = false) const; + virtual bool can_export(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates, bool p_debug = false) const; virtual bool has_valid_export_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates, bool p_debug = false) const = 0; virtual bool has_valid_project_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error) const = 0; virtual List<String> get_binary_extensions(const Ref<EditorExportPreset> &p_preset) const = 0; - virtual Error export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags = 0) = 0; - virtual Error export_pack(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags = 0); - virtual Error export_zip(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags = 0); + virtual Error export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, BitField<EditorExportPlatform::DebugFlags> p_flags = 0) = 0; + virtual Error export_pack(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, BitField<EditorExportPlatform::DebugFlags> p_flags = 0); + virtual Error export_zip(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, BitField<EditorExportPlatform::DebugFlags> p_flags = 0); virtual void get_platform_features(List<String> *r_features) const = 0; - virtual void resolve_platform_feature_priorities(const Ref<EditorExportPreset> &p_preset, HashSet<String> &p_features) = 0; + virtual void resolve_platform_feature_priorities(const Ref<EditorExportPreset> &p_preset, HashSet<String> &p_features){}; virtual String get_debug_protocol() const { return "tcp://"; } EditorExportPlatform(); }; +VARIANT_ENUM_CAST(EditorExportPlatform::ExportMessageType) +VARIANT_BITFIELD_CAST(EditorExportPlatform::DebugFlags); + #endif // EDITOR_EXPORT_PLATFORM_H diff --git a/editor/export/editor_export_platform_extension.cpp b/editor/export/editor_export_platform_extension.cpp new file mode 100644 index 0000000000..808a2076e2 --- /dev/null +++ b/editor/export/editor_export_platform_extension.cpp @@ -0,0 +1,317 @@ +/**************************************************************************/ +/* editor_export_platform_extension.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 "editor_export_platform_extension.h" + +void EditorExportPlatformExtension::_bind_methods() { + ClassDB::bind_method(D_METHOD("set_config_error", "error_text"), &EditorExportPlatformExtension::set_config_error); + ClassDB::bind_method(D_METHOD("get_config_error"), &EditorExportPlatformExtension::get_config_error); + + ClassDB::bind_method(D_METHOD("set_config_missing_templates", "missing_templates"), &EditorExportPlatformExtension::set_config_missing_templates); + ClassDB::bind_method(D_METHOD("get_config_missing_templates"), &EditorExportPlatformExtension::get_config_missing_templates); + + GDVIRTUAL_BIND(_get_preset_features, "preset"); + GDVIRTUAL_BIND(_is_executable, "path"); + GDVIRTUAL_BIND(_get_export_options); + GDVIRTUAL_BIND(_should_update_export_options); + GDVIRTUAL_BIND(_get_export_option_visibility, "preset", "option"); + GDVIRTUAL_BIND(_get_export_option_warning, "preset", "option"); + + GDVIRTUAL_BIND(_get_os_name); + GDVIRTUAL_BIND(_get_name); + GDVIRTUAL_BIND(_get_logo); + + GDVIRTUAL_BIND(_poll_export); + GDVIRTUAL_BIND(_get_options_count); + GDVIRTUAL_BIND(_get_options_tooltip); + + GDVIRTUAL_BIND(_get_option_icon, "device"); + GDVIRTUAL_BIND(_get_option_label, "device"); + GDVIRTUAL_BIND(_get_option_tooltip, "device"); + GDVIRTUAL_BIND(_get_device_architecture, "device"); + + GDVIRTUAL_BIND(_cleanup); + + GDVIRTUAL_BIND(_run, "preset", "device", "debug_flags"); + GDVIRTUAL_BIND(_get_run_icon); + + GDVIRTUAL_BIND(_can_export, "preset", "debug"); + GDVIRTUAL_BIND(_has_valid_export_configuration, "preset", "debug"); + GDVIRTUAL_BIND(_has_valid_project_configuration, "preset"); + + GDVIRTUAL_BIND(_get_binary_extensions, "preset"); + + GDVIRTUAL_BIND(_export_project, "preset", "debug", "path", "flags"); + GDVIRTUAL_BIND(_export_pack, "preset", "debug", "path", "flags"); + GDVIRTUAL_BIND(_export_zip, "preset", "debug", "path", "flags"); + + GDVIRTUAL_BIND(_get_platform_features); + + GDVIRTUAL_BIND(_get_debug_protocol); +} + +void EditorExportPlatformExtension::get_preset_features(const Ref<EditorExportPreset> &p_preset, List<String> *r_features) const { + Vector<String> ret; + if (GDVIRTUAL_REQUIRED_CALL(_get_preset_features, p_preset, ret) && r_features) { + for (const String &E : ret) { + r_features->push_back(E); + } + } +} + +bool EditorExportPlatformExtension::is_executable(const String &p_path) const { + bool ret = false; + GDVIRTUAL_CALL(_is_executable, p_path, ret); + return ret; +} + +void EditorExportPlatformExtension::get_export_options(List<ExportOption> *r_options) const { + TypedArray<Dictionary> ret; + if (GDVIRTUAL_CALL(_get_export_options, ret) && r_options) { + for (const Variant &var : ret) { + const Dictionary &d = var; + ERR_CONTINUE(!d.has("name")); + ERR_CONTINUE(!d.has("type")); + + PropertyInfo pinfo = PropertyInfo::from_dict(d); + ERR_CONTINUE(pinfo.name.is_empty() && (pinfo.usage & PROPERTY_USAGE_STORAGE)); + ERR_CONTINUE(pinfo.type < 0 || pinfo.type >= Variant::VARIANT_MAX); + + Variant default_value; + if (d.has("default_value")) { + default_value = d["default_value"]; + } + bool update_visibility = false; + if (d.has("update_visibility")) { + update_visibility = d["update_visibility"]; + } + bool required = false; + if (d.has("required")) { + required = d["required"]; + } + + r_options->push_back(ExportOption(pinfo, default_value, update_visibility, required)); + } + } +} + +bool EditorExportPlatformExtension::should_update_export_options() { + bool ret = false; + GDVIRTUAL_CALL(_should_update_export_options, ret); + return ret; +} + +bool EditorExportPlatformExtension::get_export_option_visibility(const EditorExportPreset *p_preset, const String &p_option) const { + bool ret = true; + GDVIRTUAL_CALL(_get_export_option_visibility, Ref<EditorExportPreset>(p_preset), p_option, ret); + return ret; +} + +String EditorExportPlatformExtension::get_export_option_warning(const EditorExportPreset *p_preset, const StringName &p_name) const { + String ret; + GDVIRTUAL_CALL(_get_export_option_warning, Ref<EditorExportPreset>(p_preset), p_name, ret); + return ret; +} + +String EditorExportPlatformExtension::get_os_name() const { + String ret; + GDVIRTUAL_REQUIRED_CALL(_get_os_name, ret); + return ret; +} + +String EditorExportPlatformExtension::get_name() const { + String ret; + GDVIRTUAL_REQUIRED_CALL(_get_name, ret); + return ret; +} + +Ref<Texture2D> EditorExportPlatformExtension::get_logo() const { + Ref<Texture2D> ret; + GDVIRTUAL_REQUIRED_CALL(_get_logo, ret); + return ret; +} + +bool EditorExportPlatformExtension::poll_export() { + bool ret = false; + GDVIRTUAL_CALL(_poll_export, ret); + return ret; +} + +int EditorExportPlatformExtension::get_options_count() const { + int ret = 0; + GDVIRTUAL_CALL(_get_options_count, ret); + return ret; +} + +String EditorExportPlatformExtension::get_options_tooltip() const { + String ret; + GDVIRTUAL_CALL(_get_options_tooltip, ret); + return ret; +} + +Ref<ImageTexture> EditorExportPlatformExtension::get_option_icon(int p_index) const { + Ref<ImageTexture> ret; + if (GDVIRTUAL_CALL(_get_option_icon, p_index, ret)) { + return ret; + } + return EditorExportPlatform::get_option_icon(p_index); +} + +String EditorExportPlatformExtension::get_option_label(int p_device) const { + String ret; + GDVIRTUAL_CALL(_get_option_label, p_device, ret); + return ret; +} + +String EditorExportPlatformExtension::get_option_tooltip(int p_device) const { + String ret; + GDVIRTUAL_CALL(_get_option_tooltip, p_device, ret); + return ret; +} + +String EditorExportPlatformExtension::get_device_architecture(int p_device) const { + String ret; + GDVIRTUAL_CALL(_get_device_architecture, p_device, ret); + return ret; +} + +void EditorExportPlatformExtension::cleanup() { + GDVIRTUAL_CALL(_cleanup); +} + +Error EditorExportPlatformExtension::run(const Ref<EditorExportPreset> &p_preset, int p_device, BitField<EditorExportPlatform::DebugFlags> p_debug_flags) { + Error ret = OK; + GDVIRTUAL_CALL(_run, p_preset, p_device, p_debug_flags, ret); + return ret; +} + +Ref<Texture2D> EditorExportPlatformExtension::get_run_icon() const { + Ref<Texture2D> ret; + if (GDVIRTUAL_CALL(_get_run_icon, ret)) { + return ret; + } + return EditorExportPlatform::get_run_icon(); +} + +bool EditorExportPlatformExtension::can_export(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates, bool p_debug) const { + bool ret = false; + config_error = r_error; + config_missing_templates = r_missing_templates; + if (GDVIRTUAL_CALL(_can_export, p_preset, p_debug, ret)) { + r_error = config_error; + r_missing_templates = config_missing_templates; + return ret; + } + return EditorExportPlatform::can_export(p_preset, r_error, r_missing_templates, p_debug); +} + +bool EditorExportPlatformExtension::has_valid_export_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates, bool p_debug) const { + bool ret = false; + config_error = r_error; + config_missing_templates = r_missing_templates; + if (GDVIRTUAL_REQUIRED_CALL(_has_valid_export_configuration, p_preset, p_debug, ret)) { + r_error = config_error; + r_missing_templates = config_missing_templates; + } + return ret; +} + +bool EditorExportPlatformExtension::has_valid_project_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error) const { + bool ret = false; + config_error = r_error; + if (GDVIRTUAL_REQUIRED_CALL(_has_valid_project_configuration, p_preset, ret)) { + r_error = config_error; + } + return ret; +} + +List<String> EditorExportPlatformExtension::get_binary_extensions(const Ref<EditorExportPreset> &p_preset) const { + List<String> ret_list; + Vector<String> ret; + if (GDVIRTUAL_REQUIRED_CALL(_get_binary_extensions, p_preset, ret)) { + for (const String &E : ret) { + ret_list.push_back(E); + } + } + return ret_list; +} + +Error EditorExportPlatformExtension::export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, BitField<EditorExportPlatform::DebugFlags> p_flags) { + ExportNotifier notifier(*this, p_preset, p_debug, p_path, p_flags); + + Error ret = FAILED; + GDVIRTUAL_REQUIRED_CALL(_export_project, p_preset, p_debug, p_path, p_flags, ret); + return ret; +} + +Error EditorExportPlatformExtension::export_pack(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, BitField<EditorExportPlatform::DebugFlags> p_flags) { + ExportNotifier notifier(*this, p_preset, p_debug, p_path, p_flags); + + Error ret = FAILED; + if (GDVIRTUAL_CALL(_export_pack, p_preset, p_debug, p_path, p_flags, ret)) { + return ret; + } + return save_pack(p_preset, p_debug, p_path); +} + +Error EditorExportPlatformExtension::export_zip(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, BitField<EditorExportPlatform::DebugFlags> p_flags) { + ExportNotifier notifier(*this, p_preset, p_debug, p_path, p_flags); + + Error ret = FAILED; + if (GDVIRTUAL_CALL(_export_zip, p_preset, p_debug, p_path, p_flags, ret)) { + return ret; + } + return save_zip(p_preset, p_debug, p_path); +} + +void EditorExportPlatformExtension::get_platform_features(List<String> *r_features) const { + Vector<String> ret; + if (GDVIRTUAL_REQUIRED_CALL(_get_platform_features, ret) && r_features) { + for (const String &E : ret) { + r_features->push_back(E); + } + } +} + +String EditorExportPlatformExtension::get_debug_protocol() const { + String ret; + if (GDVIRTUAL_CALL(_get_debug_protocol, ret)) { + return ret; + } + return EditorExportPlatform::get_debug_protocol(); +} + +EditorExportPlatformExtension::EditorExportPlatformExtension() { + //NOP +} + +EditorExportPlatformExtension::~EditorExportPlatformExtension() { + //NOP +} diff --git a/editor/export/editor_export_platform_extension.h b/editor/export/editor_export_platform_extension.h new file mode 100644 index 0000000000..6391e65ac1 --- /dev/null +++ b/editor/export/editor_export_platform_extension.h @@ -0,0 +1,149 @@ +/**************************************************************************/ +/* editor_export_platform_extension.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. */ +/**************************************************************************/ + +#ifndef EDITOR_EXPORT_PLATFORM_EXTENSION_H +#define EDITOR_EXPORT_PLATFORM_EXTENSION_H + +#include "editor_export_platform.h" +#include "editor_export_preset.h" + +class EditorExportPlatformExtension : public EditorExportPlatform { + GDCLASS(EditorExportPlatformExtension, EditorExportPlatform); + + mutable String config_error; + mutable bool config_missing_templates = false; + +protected: + static void _bind_methods(); + +public: + virtual void get_preset_features(const Ref<EditorExportPreset> &p_preset, List<String> *r_features) const override; + GDVIRTUAL1RC(Vector<String>, _get_preset_features, Ref<EditorExportPreset>); + + virtual bool is_executable(const String &p_path) const override; + GDVIRTUAL1RC(bool, _is_executable, const String &); + + virtual void get_export_options(List<ExportOption> *r_options) const override; + GDVIRTUAL0RC(TypedArray<Dictionary>, _get_export_options); + + virtual bool should_update_export_options() override; + GDVIRTUAL0R(bool, _should_update_export_options); + + virtual bool get_export_option_visibility(const EditorExportPreset *p_preset, const String &p_option) const override; + GDVIRTUAL2RC(bool, _get_export_option_visibility, Ref<EditorExportPreset>, const String &); + + virtual String get_export_option_warning(const EditorExportPreset *p_preset, const StringName &p_name) const override; + GDVIRTUAL2RC(String, _get_export_option_warning, Ref<EditorExportPreset>, const StringName &); + + virtual String get_os_name() const override; + GDVIRTUAL0RC(String, _get_os_name); + + virtual String get_name() const override; + GDVIRTUAL0RC(String, _get_name); + + virtual Ref<Texture2D> get_logo() const override; + GDVIRTUAL0RC(Ref<Texture2D>, _get_logo); + + virtual bool poll_export() override; + GDVIRTUAL0R(bool, _poll_export); + + virtual int get_options_count() const override; + GDVIRTUAL0RC(int, _get_options_count); + + virtual String get_options_tooltip() const override; + GDVIRTUAL0RC(String, _get_options_tooltip); + + virtual Ref<ImageTexture> get_option_icon(int p_index) const override; + GDVIRTUAL1RC(Ref<ImageTexture>, _get_option_icon, int); + + virtual String get_option_label(int p_device) const override; + GDVIRTUAL1RC(String, _get_option_label, int); + + virtual String get_option_tooltip(int p_device) const override; + GDVIRTUAL1RC(String, _get_option_tooltip, int); + + virtual String get_device_architecture(int p_device) const override; + GDVIRTUAL1RC(String, _get_device_architecture, int); + + virtual void cleanup() override; + GDVIRTUAL0(_cleanup); + + virtual Error run(const Ref<EditorExportPreset> &p_preset, int p_device, BitField<EditorExportPlatform::DebugFlags> p_debug_flags) override; + GDVIRTUAL3R(Error, _run, Ref<EditorExportPreset>, int, BitField<EditorExportPlatform::DebugFlags>); + + virtual Ref<Texture2D> get_run_icon() const override; + GDVIRTUAL0RC(Ref<Texture2D>, _get_run_icon); + + void set_config_error(const String &p_error) const { + config_error = p_error; + } + String get_config_error() const { + return config_error; + } + + void set_config_missing_templates(bool p_missing_templates) const { + config_missing_templates = p_missing_templates; + } + bool get_config_missing_templates() const { + return config_missing_templates; + } + + virtual bool can_export(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates, bool p_debug = false) const override; + GDVIRTUAL2RC(bool, _can_export, Ref<EditorExportPreset>, bool); + + virtual bool has_valid_export_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates, bool p_debug = false) const override; + GDVIRTUAL2RC(bool, _has_valid_export_configuration, Ref<EditorExportPreset>, bool); + + virtual bool has_valid_project_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error) const override; + GDVIRTUAL1RC(bool, _has_valid_project_configuration, Ref<EditorExportPreset>); + + virtual List<String> get_binary_extensions(const Ref<EditorExportPreset> &p_preset) const override; + GDVIRTUAL1RC(Vector<String>, _get_binary_extensions, Ref<EditorExportPreset>); + + virtual Error export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, BitField<EditorExportPlatform::DebugFlags> p_flags = 0) override; + GDVIRTUAL4R(Error, _export_project, Ref<EditorExportPreset>, bool, const String &, BitField<EditorExportPlatform::DebugFlags>); + + virtual Error export_pack(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, BitField<EditorExportPlatform::DebugFlags> p_flags = 0) override; + GDVIRTUAL4R(Error, _export_pack, Ref<EditorExportPreset>, bool, const String &, BitField<EditorExportPlatform::DebugFlags>); + + virtual Error export_zip(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, BitField<EditorExportPlatform::DebugFlags> p_flags = 0) override; + GDVIRTUAL4R(Error, _export_zip, Ref<EditorExportPreset>, bool, const String &, BitField<EditorExportPlatform::DebugFlags>); + + virtual void get_platform_features(List<String> *r_features) const override; + GDVIRTUAL0RC(Vector<String>, _get_platform_features); + + virtual String get_debug_protocol() const override; + GDVIRTUAL0RC(String, _get_debug_protocol); + + EditorExportPlatformExtension(); + ~EditorExportPlatformExtension(); +}; + +#endif // EDITOR_EXPORT_PLATFORM_EXTENSION_H diff --git a/editor/export/editor_export_platform_pc.cpp b/editor/export/editor_export_platform_pc.cpp index cdaf18b346..24d89b7f34 100644 --- a/editor/export/editor_export_platform_pc.cpp +++ b/editor/export/editor_export_platform_pc.cpp @@ -115,7 +115,7 @@ bool EditorExportPlatformPC::has_valid_project_configuration(const Ref<EditorExp return true; } -Error EditorExportPlatformPC::export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags) { +Error EditorExportPlatformPC::export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, BitField<EditorExportPlatform::DebugFlags> p_flags) { ExportNotifier notifier(*this, p_preset, p_debug, p_path, p_flags); Error err = prepare_template(p_preset, p_debug, p_path, p_flags); @@ -129,7 +129,7 @@ Error EditorExportPlatformPC::export_project(const Ref<EditorExportPreset> &p_pr return err; } -Error EditorExportPlatformPC::prepare_template(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags) { +Error EditorExportPlatformPC::prepare_template(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, BitField<EditorExportPlatform::DebugFlags> p_flags) { if (!DirAccess::exists(p_path.get_base_dir())) { add_message(EXPORT_MESSAGE_ERROR, TTR("Prepare Template"), TTR("The given export path doesn't exist.")); return ERR_FILE_BAD_PATH; @@ -182,7 +182,7 @@ Error EditorExportPlatformPC::prepare_template(const Ref<EditorExportPreset> &p_ return err; } -Error EditorExportPlatformPC::export_project_data(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags) { +Error EditorExportPlatformPC::export_project_data(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, BitField<EditorExportPlatform::DebugFlags> p_flags) { String pck_path; if (p_preset->get("binary_format/embed_pck")) { pck_path = p_path; diff --git a/editor/export/editor_export_platform_pc.h b/editor/export/editor_export_platform_pc.h index 53574c2333..668ddaf47e 100644 --- a/editor/export/editor_export_platform_pc.h +++ b/editor/export/editor_export_platform_pc.h @@ -54,13 +54,13 @@ public: virtual bool has_valid_export_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates, bool p_debug = false) const override; virtual bool has_valid_project_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error) const override; - virtual Error export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags = 0) override; + virtual Error export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, BitField<EditorExportPlatform::DebugFlags> p_flags = 0) override; virtual Error sign_shared_object(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path); virtual String get_template_file_name(const String &p_target, const String &p_arch) const = 0; - virtual Error prepare_template(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags); - virtual Error modify_template(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags) { return OK; }; - virtual Error export_project_data(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags); + virtual Error prepare_template(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, BitField<EditorExportPlatform::DebugFlags> p_flags); + virtual Error modify_template(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, BitField<EditorExportPlatform::DebugFlags> p_flags) { return OK; } + virtual Error export_project_data(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, BitField<EditorExportPlatform::DebugFlags> p_flags); void set_name(const String &p_name); void set_os_name(const String &p_name); diff --git a/editor/export/editor_export_plugin.cpp b/editor/export/editor_export_plugin.cpp index 28d0750d5a..f56dd61e3b 100644 --- a/editor/export/editor_export_plugin.cpp +++ b/editor/export/editor_export_plugin.cpp @@ -48,6 +48,14 @@ Ref<EditorExportPreset> EditorExportPlugin::get_export_preset() const { return export_preset; } +Ref<EditorExportPlatform> EditorExportPlugin::get_export_platform() const { + if (export_preset.is_valid()) { + return export_preset->get_platform(); + } else { + return Ref<EditorExportPlatform>(); + } +} + void EditorExportPlugin::add_file(const String &p_path, const Vector<uint8_t> &p_file, bool p_remap) { ExtraFile ef; ef.data = p_file; @@ -321,6 +329,9 @@ void EditorExportPlugin::_bind_methods() { ClassDB::bind_method(D_METHOD("skip"), &EditorExportPlugin::skip); ClassDB::bind_method(D_METHOD("get_option", "name"), &EditorExportPlugin::get_option); + ClassDB::bind_method(D_METHOD("get_export_preset"), &EditorExportPlugin::get_export_preset); + ClassDB::bind_method(D_METHOD("get_export_platform"), &EditorExportPlugin::get_export_platform); + GDVIRTUAL_BIND(_export_file, "path", "type", "features"); GDVIRTUAL_BIND(_export_begin, "features", "is_debug", "path", "flags"); GDVIRTUAL_BIND(_export_end); diff --git a/editor/export/editor_export_plugin.h b/editor/export/editor_export_plugin.h index 56eea85010..7a355614c7 100644 --- a/editor/export/editor_export_plugin.h +++ b/editor/export/editor_export_plugin.h @@ -91,6 +91,7 @@ class EditorExportPlugin : public RefCounted { protected: void set_export_preset(const Ref<EditorExportPreset> &p_preset); Ref<EditorExportPreset> get_export_preset() const; + Ref<EditorExportPlatform> get_export_platform() const; void add_file(const String &p_path, const Vector<uint8_t> &p_file, bool p_remap); void add_shared_object(const String &p_path, const Vector<String> &tags, const String &p_target = String()); diff --git a/editor/export/editor_export_preset.cpp b/editor/export/editor_export_preset.cpp index e2e3e9d154..9f805666d0 100644 --- a/editor/export/editor_export_preset.cpp +++ b/editor/export/editor_export_preset.cpp @@ -62,6 +62,48 @@ bool EditorExportPreset::_get(const StringName &p_name, Variant &r_ret) const { void EditorExportPreset::_bind_methods() { ClassDB::bind_method(D_METHOD("_get_property_warning", "name"), &EditorExportPreset::_get_property_warning); + + ClassDB::bind_method(D_METHOD("has", "property"), &EditorExportPreset::has); + + ClassDB::bind_method(D_METHOD("get_files_to_export"), &EditorExportPreset::get_files_to_export); + ClassDB::bind_method(D_METHOD("get_customized_files"), &EditorExportPreset::get_customized_files); + ClassDB::bind_method(D_METHOD("get_customized_files_count"), &EditorExportPreset::get_customized_files_count); + ClassDB::bind_method(D_METHOD("has_export_file", "path"), &EditorExportPreset::has_export_file); + ClassDB::bind_method(D_METHOD("get_file_export_mode", "path", "default"), &EditorExportPreset::get_file_export_mode, DEFVAL(MODE_FILE_NOT_CUSTOMIZED)); + + ClassDB::bind_method(D_METHOD("get_preset_name"), &EditorExportPreset::get_name); + ClassDB::bind_method(D_METHOD("is_runnable"), &EditorExportPreset::is_runnable); + ClassDB::bind_method(D_METHOD("are_advanced_options_enabled"), &EditorExportPreset::are_advanced_options_enabled); + ClassDB::bind_method(D_METHOD("is_dedicated_server"), &EditorExportPreset::is_dedicated_server); + ClassDB::bind_method(D_METHOD("get_export_filter"), &EditorExportPreset::get_export_filter); + ClassDB::bind_method(D_METHOD("get_include_filter"), &EditorExportPreset::get_include_filter); + ClassDB::bind_method(D_METHOD("get_exclude_filter"), &EditorExportPreset::get_exclude_filter); + ClassDB::bind_method(D_METHOD("get_custom_features"), &EditorExportPreset::get_custom_features); + ClassDB::bind_method(D_METHOD("get_export_path"), &EditorExportPreset::get_export_path); + ClassDB::bind_method(D_METHOD("get_encryption_in_filter"), &EditorExportPreset::get_enc_in_filter); + ClassDB::bind_method(D_METHOD("get_encryption_ex_filter"), &EditorExportPreset::get_enc_ex_filter); + ClassDB::bind_method(D_METHOD("get_encrypt_pck"), &EditorExportPreset::get_enc_pck); + ClassDB::bind_method(D_METHOD("get_encrypt_directory"), &EditorExportPreset::get_enc_directory); + ClassDB::bind_method(D_METHOD("get_encryption_key"), &EditorExportPreset::get_script_encryption_key); + ClassDB::bind_method(D_METHOD("get_script_export_mode"), &EditorExportPreset::get_script_export_mode); + + ClassDB::bind_method(D_METHOD("get_or_env", "name", "env_var"), &EditorExportPreset::_get_or_env); + ClassDB::bind_method(D_METHOD("get_version", "name", "windows_version"), &EditorExportPreset::get_version); + + BIND_ENUM_CONSTANT(EXPORT_ALL_RESOURCES); + BIND_ENUM_CONSTANT(EXPORT_SELECTED_SCENES); + BIND_ENUM_CONSTANT(EXPORT_SELECTED_RESOURCES); + BIND_ENUM_CONSTANT(EXCLUDE_SELECTED_RESOURCES); + BIND_ENUM_CONSTANT(EXPORT_CUSTOMIZED); + + BIND_ENUM_CONSTANT(MODE_FILE_NOT_CUSTOMIZED); + BIND_ENUM_CONSTANT(MODE_FILE_STRIP); + BIND_ENUM_CONSTANT(MODE_FILE_KEEP); + BIND_ENUM_CONSTANT(MODE_FILE_REMOVE); + + BIND_ENUM_CONSTANT(MODE_SCRIPT_TEXT); + BIND_ENUM_CONSTANT(MODE_SCRIPT_BINARY_TOKENS); + BIND_ENUM_CONSTANT(MODE_SCRIPT_BINARY_TOKENS_COMPRESSED); } String EditorExportPreset::_get_property_warning(const StringName &p_name) const { diff --git a/editor/export/editor_export_preset.h b/editor/export/editor_export_preset.h index c6a8808af1..f220477461 100644 --- a/editor/export/editor_export_preset.h +++ b/editor/export/editor_export_preset.h @@ -168,6 +168,9 @@ public: void set_script_export_mode(int p_mode); int get_script_export_mode() const; + Variant _get_or_env(const StringName &p_name, const String &p_env_var) const { + return get_or_env(p_name, p_env_var); + } Variant get_or_env(const StringName &p_name, const String &p_env_var, bool *r_valid = nullptr) const; // Return the preset's version number, or fall back to the @@ -183,4 +186,8 @@ public: EditorExportPreset(); }; +VARIANT_ENUM_CAST(EditorExportPreset::ExportFilter); +VARIANT_ENUM_CAST(EditorExportPreset::FileExportMode); +VARIANT_ENUM_CAST(EditorExportPreset::ScriptExportMode); + #endif // EDITOR_EXPORT_PRESET_H diff --git a/editor/export/export_template_manager.cpp b/editor/export/export_template_manager.cpp index 0caf0ee066..2e4f2da9a8 100644 --- a/editor/export/export_template_manager.cpp +++ b/editor/export/export_template_manager.cpp @@ -47,6 +47,32 @@ #include "scene/gui/tree.h" #include "scene/main/http_request.h" +enum DownloadsAvailability { + DOWNLOADS_AVAILABLE, + DOWNLOADS_NOT_AVAILABLE_IN_OFFLINE_MODE, + DOWNLOADS_NOT_AVAILABLE_FOR_DEV_BUILDS, +}; + +static DownloadsAvailability _get_downloads_availability() { + const int network_mode = EDITOR_GET("network/connection/network_mode"); + if (network_mode == EditorSettings::NETWORK_OFFLINE) { + return DOWNLOADS_NOT_AVAILABLE_IN_OFFLINE_MODE; + } + + // Downloadable export templates are only available for stable and official alpha/beta/RC builds + // (which always have a number following their status, e.g. "alpha1"). + // Therefore, don't display download-related features when using a development version + // (whose builds aren't numbered). + if (String(VERSION_STATUS) == String("dev") || + String(VERSION_STATUS) == String("alpha") || + String(VERSION_STATUS) == String("beta") || + String(VERSION_STATUS) == String("rc")) { + return DOWNLOADS_NOT_AVAILABLE_FOR_DEV_BUILDS; + } + + return DOWNLOADS_AVAILABLE; +} + void ExportTemplateManager::_update_template_status() { // Fetch installed templates from the file system. Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); @@ -111,7 +137,9 @@ void ExportTemplateManager::_update_template_status() { TreeItem *ti = installed_table->create_item(installed_root); ti->set_text(0, version_string); +#ifndef ANDROID_ENABLED ti->add_button(0, get_editor_theme_icon(SNAME("Folder")), OPEN_TEMPLATE_FOLDER, false, TTR("Open the folder containing these templates.")); +#endif ti->add_button(0, get_editor_theme_icon(SNAME("Remove")), UNINSTALL_TEMPLATE, false, TTR("Uninstall these templates.")); } } @@ -639,9 +667,55 @@ void ExportTemplateManager::_open_template_folder(const String &p_version) { void ExportTemplateManager::popup_manager() { _update_template_status(); - if (downloads_available && !is_downloading_templates) { - _refresh_mirrors(); + + switch (_get_downloads_availability()) { + case DOWNLOADS_AVAILABLE: { + current_missing_label->set_text(TTR("Export templates are missing. Download them or install from a file.")); + + mirrors_list->clear(); + mirrors_list->add_item(TTR("Best available mirror"), 0); + mirrors_list->set_disabled(false); + mirrors_list->set_tooltip_text(""); + + mirror_options_button->set_disabled(false); + + download_current_button->set_disabled(false); + download_current_button->set_tooltip_text(""); + + if (!is_downloading_templates) { + _refresh_mirrors(); + } + } break; + + case DOWNLOADS_NOT_AVAILABLE_IN_OFFLINE_MODE: { + current_missing_label->set_text(TTR("Export templates are missing. Install them from a file.")); + + mirrors_list->clear(); + mirrors_list->add_item(TTR("Not available in offline mode"), 0); + mirrors_list->set_disabled(true); + mirrors_list->set_tooltip_text(TTR("Template downloading is disabled in offline mode.")); + + mirror_options_button->set_disabled(true); + + download_current_button->set_disabled(true); + download_current_button->set_tooltip_text(TTR("Template downloading is disabled in offline mode.")); + } break; + + case DOWNLOADS_NOT_AVAILABLE_FOR_DEV_BUILDS: { + current_missing_label->set_text(TTR("Export templates are missing. Install them from a file.")); + + mirrors_list->clear(); + mirrors_list->add_item(TTR("No templates for development builds"), 0); + mirrors_list->set_disabled(true); + mirrors_list->set_tooltip_text(TTR("Official export templates aren't available for development builds.")); + + mirror_options_button->set_disabled(true); + + download_current_button->set_disabled(true); + download_current_button->set_tooltip_text(TTR("Official export templates aren't available for development builds.")); + } break; } + popup_centered(Size2(720, 280) * EDSCALE); } @@ -864,16 +938,6 @@ ExportTemplateManager::ExportTemplateManager() { set_hide_on_ok(false); set_ok_button_text(TTR("Close")); - // Downloadable export templates are only available for stable and official alpha/beta/RC builds - // (which always have a number following their status, e.g. "alpha1"). - // Therefore, don't display download-related features when using a development version - // (whose builds aren't numbered). - downloads_available = - String(VERSION_STATUS) != String("dev") && - String(VERSION_STATUS) != String("alpha") && - String(VERSION_STATUS) != String("beta") && - String(VERSION_STATUS) != String("rc"); - VBoxContainer *main_vb = memnew(VBoxContainer); add_child(main_vb); @@ -896,11 +960,6 @@ ExportTemplateManager::ExportTemplateManager() { current_missing_label->set_h_size_flags(Control::SIZE_EXPAND_FILL); current_missing_label->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_RIGHT); - if (downloads_available) { - current_missing_label->set_text(TTR("Export templates are missing. Download them or install from a file.")); - } else { - current_missing_label->set_text(TTR("Export templates are missing. Install them from a file.")); - } current_hb->add_child(current_missing_label); // Status: Current version is installed. @@ -921,11 +980,13 @@ ExportTemplateManager::ExportTemplateManager() { current_installed_path->set_h_size_flags(Control::SIZE_EXPAND_FILL); current_installed_hb->add_child(current_installed_path); - current_open_button = memnew(Button); +#ifndef ANDROID_ENABLED + Button *current_open_button = memnew(Button); current_open_button->set_text(TTR("Open Folder")); current_open_button->set_tooltip_text(TTR("Open the folder containing installed templates for the current version.")); current_installed_hb->add_child(current_open_button); current_open_button->connect(SceneStringName(pressed), callable_mp(this, &ExportTemplateManager::_open_template_folder).bind(VERSION_FULL_CONFIG)); +#endif current_uninstall_button = memnew(Button); current_uninstall_button->set_text(TTR("Uninstall")); @@ -953,12 +1014,6 @@ ExportTemplateManager::ExportTemplateManager() { mirrors_list = memnew(OptionButton); mirrors_list->set_custom_minimum_size(Size2(280, 0) * EDSCALE); - if (downloads_available) { - mirrors_list->add_item(TTR("Best available mirror"), 0); - } else { - mirrors_list->add_item(TTR("(no templates for development builds)"), 0); - mirrors_list->set_disabled(true); - } download_install_hb->add_child(mirrors_list); request_mirrors = memnew(HTTPRequest); @@ -968,24 +1023,17 @@ ExportTemplateManager::ExportTemplateManager() { mirror_options_button = memnew(MenuButton); mirror_options_button->get_popup()->add_item(TTR("Open in Web Browser"), VISIT_WEB_MIRROR); mirror_options_button->get_popup()->add_item(TTR("Copy Mirror URL"), COPY_MIRROR_URL); - mirror_options_button->set_disabled(!downloads_available); download_install_hb->add_child(mirror_options_button); mirror_options_button->get_popup()->connect(SceneStringName(id_pressed), callable_mp(this, &ExportTemplateManager::_mirror_options_button_cbk)); download_install_hb->add_spacer(); - Button *download_current_button = memnew(Button); + download_current_button = memnew(Button); download_current_button->set_text(TTR("Download and Install")); download_current_button->set_tooltip_text(TTR("Download and install templates for the current version from the best possible mirror.")); download_install_hb->add_child(download_current_button); download_current_button->connect(SceneStringName(pressed), callable_mp(this, &ExportTemplateManager::_download_current)); - // Update downloads buttons to prevent unsupported downloads. - if (!downloads_available) { - download_current_button->set_disabled(true); - download_current_button->set_tooltip_text(TTR("Official export templates aren't available for development builds.")); - } - HBoxContainer *install_file_hb = memnew(HBoxContainer); install_file_hb->set_alignment(BoxContainer::ALIGNMENT_END); install_options_vb->add_child(install_file_hb); diff --git a/editor/export/export_template_manager.h b/editor/export/export_template_manager.h index b1c5855878..ccfe568d32 100644 --- a/editor/export/export_template_manager.h +++ b/editor/export/export_template_manager.h @@ -46,7 +46,6 @@ class ExportTemplateManager : public AcceptDialog { GDCLASS(ExportTemplateManager, AcceptDialog); bool current_version_exists = false; - bool downloads_available = true; bool mirrors_available = false; bool is_refreshing_mirrors = false; bool is_downloading_templates = false; @@ -58,7 +57,6 @@ class ExportTemplateManager : public AcceptDialog { HBoxContainer *current_installed_hb = nullptr; LineEdit *current_installed_path = nullptr; - Button *current_open_button = nullptr; Button *current_uninstall_button = nullptr; VBoxContainer *install_options_vb = nullptr; @@ -75,6 +73,7 @@ class ExportTemplateManager : public AcceptDialog { Label *download_progress_label = nullptr; HTTPRequest *download_templates = nullptr; Button *install_file_button = nullptr; + Button *download_current_button = nullptr; HTTPRequest *request_mirrors = nullptr; enum TemplatesAction { diff --git a/editor/export/project_export.cpp b/editor/export/project_export.cpp index 3103e504b9..03e9fba12d 100644 --- a/editor/export/project_export.cpp +++ b/editor/export/project_export.cpp @@ -1147,10 +1147,8 @@ void ProjectExportDialog::_export_project_to_path(const String &p_path) { } void ProjectExportDialog::_export_all_dialog() { -#ifndef ANDROID_ENABLED export_all_dialog->show(); export_all_dialog->popup_centered(Size2(300, 80)); -#endif } void ProjectExportDialog::_export_all_dialog_action(const String &p_str) { @@ -1412,12 +1410,12 @@ ProjectExportDialog::ProjectExportDialog() { sec_scroll_container->add_child(sec_vb); enc_pck = memnew(CheckButton); - enc_pck->connect("toggled", callable_mp(this, &ProjectExportDialog::_enc_pck_changed)); + enc_pck->connect(SceneStringName(toggled), callable_mp(this, &ProjectExportDialog::_enc_pck_changed)); enc_pck->set_text(TTR("Encrypt Exported PCK")); sec_vb->add_child(enc_pck); enc_directory = memnew(CheckButton); - enc_directory->connect("toggled", callable_mp(this, &ProjectExportDialog::_enc_directory_changed)); + enc_directory->connect(SceneStringName(toggled), callable_mp(this, &ProjectExportDialog::_enc_directory_changed)); enc_directory->set_text(TTR("Encrypt Index (File Names and Info)")); sec_vb->add_child(enc_directory); @@ -1491,13 +1489,9 @@ ProjectExportDialog::ProjectExportDialog() { set_ok_button_text(TTR("Export PCK/ZIP...")); get_ok_button()->set_tooltip_text(TTR("Export the project resources as a PCK or ZIP package. This is not a playable build, only the project data without a Godot executable.")); get_ok_button()->set_disabled(true); -#ifdef ANDROID_ENABLED - export_button = memnew(Button); - export_button->hide(); -#else + export_button = add_button(TTR("Export Project..."), !DisplayServer::get_singleton()->get_swap_cancel_ok(), "export"); export_button->set_tooltip_text(TTR("Export the project as a playable build (Godot executable and project data) for the selected preset.")); -#endif export_button->connect(SceneStringName(pressed), callable_mp(this, &ProjectExportDialog::_export_project)); // Disable initially before we select a valid preset export_button->set_disabled(true); @@ -1510,14 +1504,8 @@ ProjectExportDialog::ProjectExportDialog() { export_all_dialog->add_button(TTR("Debug"), true, "debug"); export_all_dialog->add_button(TTR("Release"), true, "release"); export_all_dialog->connect("custom_action", callable_mp(this, &ProjectExportDialog::_export_all_dialog_action)); -#ifdef ANDROID_ENABLED - export_all_dialog->hide(); - export_all_button = memnew(Button); - export_all_button->hide(); -#else export_all_button = add_button(TTR("Export All..."), !DisplayServer::get_singleton()->get_swap_cancel_ok(), "export"); -#endif export_all_button->connect(SceneStringName(pressed), callable_mp(this, &ProjectExportDialog::_export_all_dialog)); export_all_button->set_disabled(true); diff --git a/editor/filesystem_dock.cpp b/editor/filesystem_dock.cpp index a9de8e3bc5..bfb7123925 100644 --- a/editor/filesystem_dock.cpp +++ b/editor/filesystem_dock.cpp @@ -1216,7 +1216,7 @@ void FileSystemDock::_select_file(const String &p_path, bool p_select_in_favorit } if (is_imported) { - SceneImportSettingsDialog::get_singleton()->open_settings(p_path, resource_type == "AnimationLibrary"); + SceneImportSettingsDialog::get_singleton()->open_settings(p_path, resource_type); } else if (resource_type == "PackedScene") { EditorNode::get_singleton()->open_request(fpath); } else { diff --git a/editor/groups_editor.cpp b/editor/groups_editor.cpp index a5f7e8556c..ce2dbe7cb1 100644 --- a/editor/groups_editor.cpp +++ b/editor/groups_editor.cpp @@ -648,7 +648,7 @@ void GroupsEditor::_show_add_group_dialog() { add_group_description->set_editable(false); gc->add_child(add_group_description); - global_group_button->connect("toggled", callable_mp(add_group_description, &LineEdit::set_editable)); + global_group_button->connect(SceneStringName(toggled), callable_mp(add_group_description, &LineEdit::set_editable)); add_group_dialog->register_text_enter(add_group_name); add_group_dialog->register_text_enter(add_group_description); diff --git a/editor/gui/editor_bottom_panel.cpp b/editor/gui/editor_bottom_panel.cpp index 3cb95a3926..4b2fd9cb2f 100644 --- a/editor/gui/editor_bottom_panel.cpp +++ b/editor/gui/editor_bottom_panel.cpp @@ -158,7 +158,7 @@ void EditorBottomPanel::load_layout_from_config(Ref<ConfigFile> p_config_file, c Button *EditorBottomPanel::add_item(String p_text, Control *p_item, const Ref<Shortcut> &p_shortcut, bool p_at_front) { Button *tb = memnew(Button); tb->set_theme_type_variation("BottomPanelButton"); - tb->connect("toggled", callable_mp(this, &EditorBottomPanel::_switch_by_control).bind(p_item)); + tb->connect(SceneStringName(toggled), callable_mp(this, &EditorBottomPanel::_switch_by_control).bind(p_item)); tb->set_drag_forwarding(Callable(), callable_mp(this, &EditorBottomPanel::_button_drag_hover).bind(tb, p_item), Callable()); tb->set_text(p_text); tb->set_shortcut(p_shortcut); @@ -295,5 +295,5 @@ EditorBottomPanel::EditorBottomPanel() { expand_button->set_theme_type_variation("FlatMenuButton"); expand_button->set_toggle_mode(true); expand_button->set_shortcut(ED_SHORTCUT_AND_COMMAND("editor/bottom_panel_expand", TTR("Expand Bottom Panel"), KeyModifierMask::SHIFT | Key::F12)); - expand_button->connect("toggled", callable_mp(this, &EditorBottomPanel::_expand_button_toggled)); + expand_button->connect(SceneStringName(toggled), callable_mp(this, &EditorBottomPanel::_expand_button_toggled)); } diff --git a/editor/gui/editor_file_dialog.cpp b/editor/gui/editor_file_dialog.cpp index afc6d58d63..7aa19509e1 100644 --- a/editor/gui/editor_file_dialog.cpp +++ b/editor/gui/editor_file_dialog.cpp @@ -1779,7 +1779,7 @@ void EditorFileDialog::_update_option_controls() { CheckBox *cb = memnew(CheckBox); cb->set_pressed(opt.default_idx); grid_options->add_child(cb); - cb->connect("toggled", callable_mp(this, &EditorFileDialog::_option_changed_checkbox_toggled).bind(opt.name)); + cb->connect(SceneStringName(toggled), callable_mp(this, &EditorFileDialog::_option_changed_checkbox_toggled).bind(opt.name)); selected_options[opt.name] = (bool)opt.default_idx; } else { OptionButton *ob = memnew(OptionButton); @@ -2146,7 +2146,7 @@ EditorFileDialog::EditorFileDialog() { show_hidden->set_toggle_mode(true); show_hidden->set_pressed(is_showing_hidden_files()); show_hidden->set_tooltip_text(TTR("Toggle the visibility of hidden files.")); - show_hidden->connect("toggled", callable_mp(this, &EditorFileDialog::set_show_hidden_files)); + show_hidden->connect(SceneStringName(toggled), callable_mp(this, &EditorFileDialog::set_show_hidden_files)); pathhb->add_child(show_hidden); pathhb->add_child(memnew(VSeparator)); diff --git a/editor/gui/editor_run_bar.cpp b/editor/gui/editor_run_bar.cpp index 4cc2d1145e..9050ee0cd4 100644 --- a/editor/gui/editor_run_bar.cpp +++ b/editor/gui/editor_run_bar.cpp @@ -445,7 +445,7 @@ EditorRunBar::EditorRunBar() { write_movie_button->set_pressed(false); write_movie_button->set_focus_mode(Control::FOCUS_NONE); write_movie_button->set_tooltip_text(TTR("Enable Movie Maker mode.\nThe project will run at stable FPS and the visual and audio output will be recorded to a video file.")); - write_movie_button->connect("toggled", callable_mp(this, &EditorRunBar::_write_movie_toggled)); + write_movie_button->connect(SceneStringName(toggled), callable_mp(this, &EditorRunBar::_write_movie_toggled)); quick_run = memnew(EditorQuickOpen); add_child(quick_run); diff --git a/editor/gui/editor_spin_slider.cpp b/editor/gui/editor_spin_slider.cpp index 9f9bdb37b3..5372d33b4c 100644 --- a/editor/gui/editor_spin_slider.cpp +++ b/editor/gui/editor_spin_slider.cpp @@ -35,6 +35,7 @@ #include "core/os/keyboard.h" #include "editor/editor_settings.h" #include "editor/themes/editor_scale.h" +#include "scene/theme/theme_db.h" bool EditorSpinSlider::is_text_field() const { return true; @@ -383,7 +384,7 @@ void EditorSpinSlider::_draw_spin_slider() { if (!hide_slider) { if (get_step() == 1) { - Ref<Texture2D> updown2 = get_theme_icon(is_read_only() ? SNAME("updown_disabled") : SNAME("updown"), SNAME("SpinBox")); + Ref<Texture2D> updown2 = is_read_only() ? theme_cache.updown_disabled_icon : theme_cache.updown_icon; int updown_vofs = (size.height - updown2->get_height()) / 2; if (rtl) { updown_offset = sb->get_margin(SIDE_LEFT); @@ -701,6 +702,9 @@ void EditorSpinSlider::_bind_methods() { ADD_SIGNAL(MethodInfo("ungrabbed")); ADD_SIGNAL(MethodInfo("value_focus_entered")); ADD_SIGNAL(MethodInfo("value_focus_exited")); + + BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_ICON, EditorSpinSlider, updown_icon, "updown"); + BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_ICON, EditorSpinSlider, updown_disabled_icon, "updown_disabled"); } void EditorSpinSlider::_ensure_input_popup() { diff --git a/editor/gui/editor_spin_slider.h b/editor/gui/editor_spin_slider.h index a0c0685629..2476c2f71b 100644 --- a/editor/gui/editor_spin_slider.h +++ b/editor/gui/editor_spin_slider.h @@ -87,6 +87,11 @@ class EditorSpinSlider : public Range { void _ensure_input_popup(); void _draw_spin_slider(); + struct ThemeCache { + Ref<Texture2D> updown_icon; + Ref<Texture2D> updown_disabled_icon; + } theme_cache; + protected: void _notification(int p_what); virtual void gui_input(const Ref<InputEvent> &p_event) override; diff --git a/editor/gui/editor_zoom_widget.cpp b/editor/gui/editor_zoom_widget.cpp index 341da7bfaf..50a4f020ab 100644 --- a/editor/gui/editor_zoom_widget.cpp +++ b/editor/gui/editor_zoom_widget.cpp @@ -141,22 +141,15 @@ void EditorZoomWidget::set_zoom_by_increments(int p_increment_count, bool p_inte } } } else { - // Base increment factor defined as the twelveth root of two. - // This allows for a smooth geometric evolution of the zoom, with the advantage of - // visiting all integer power of two scale factors. - // Note: this is analogous to the 'semitone' interval in the music world - // In order to avoid numerical imprecisions, we compute and edit a zoom index - // with the following relation: zoom = 2 ^ (index / 12) - if (zoom < CMP_EPSILON || p_increment_count == 0) { return; } - // zoom = 2**(index/12) => log2(zoom) = index/12 - float closest_zoom_index = Math::round(Math::log(zoom_noscale) * 12.f / Math::log(2.f)); - - float new_zoom_index = closest_zoom_index + p_increment_count; - float new_zoom = Math::pow(2.f, new_zoom_index / 12.f); + // Zoom is calculated as pow(zoom_factor, zoom_step). + // This ensures the zoom will always equal 100% when zoom_step is 0. + float zoom_factor = EDITOR_GET("editors/2d/zoom_speed_factor"); + float current_zoom_step = Math::round(Math::log(zoom_noscale) / Math::log(zoom_factor)); + float new_zoom = Math::pow(zoom_factor, current_zoom_step + p_increment_count); // Restore Editor scale transformation. new_zoom *= MAX(1, EDSCALE); diff --git a/editor/gui/scene_tree_editor.cpp b/editor/gui/scene_tree_editor.cpp index 87d8ddad09..52ba98b4d5 100644 --- a/editor/gui/scene_tree_editor.cpp +++ b/editor/gui/scene_tree_editor.cpp @@ -216,6 +216,7 @@ void SceneTreeEditor::_add_nodes(Node *p_node, TreeItem *p_parent) { TreeItem *item = tree->create_item(p_parent); item->set_text(0, p_node->get_name()); + item->set_text_overrun_behavior(0, TextServer::OVERRUN_NO_TRIMMING); if (can_rename && !part_of_subscene) { item->set_editable(0, true); } @@ -1768,7 +1769,7 @@ SceneTreeDialog::SceneTreeDialog() { // Add 'Show All' button to HBoxContainer next to the filter, visible only when valid_types is defined. show_all_nodes = memnew(CheckButton); show_all_nodes->set_text(TTR("Show All")); - show_all_nodes->connect("toggled", callable_mp(this, &SceneTreeDialog::_show_all_nodes_changed)); + show_all_nodes->connect(SceneStringName(toggled), callable_mp(this, &SceneTreeDialog::_show_all_nodes_changed)); show_all_nodes->set_h_size_flags(Control::SIZE_SHRINK_BEGIN); show_all_nodes->hide(); filter_hbc->add_child(show_all_nodes); diff --git a/editor/history_dock.cpp b/editor/history_dock.cpp index 5a64fba788..1a0971a15c 100644 --- a/editor/history_dock.cpp +++ b/editor/history_dock.cpp @@ -245,8 +245,8 @@ HistoryDock::HistoryDock() { current_scene_checkbox->set_text(TTR("Scene")); current_scene_checkbox->set_h_size_flags(SIZE_EXPAND_FILL); current_scene_checkbox->set_clip_text(true); - current_scene_checkbox->connect("toggled", callable_mp(this, &HistoryDock::refresh_history).unbind(1)); - current_scene_checkbox->connect("toggled", callable_mp(this, &HistoryDock::save_options).unbind(1)); + current_scene_checkbox->connect(SceneStringName(toggled), callable_mp(this, &HistoryDock::refresh_history).unbind(1)); + current_scene_checkbox->connect(SceneStringName(toggled), callable_mp(this, &HistoryDock::save_options).unbind(1)); global_history_checkbox = memnew(CheckBox); mode_hb->add_child(global_history_checkbox); @@ -255,8 +255,8 @@ HistoryDock::HistoryDock() { global_history_checkbox->set_text(TTR("Global")); global_history_checkbox->set_h_size_flags(SIZE_EXPAND_FILL); global_history_checkbox->set_clip_text(true); - global_history_checkbox->connect("toggled", callable_mp(this, &HistoryDock::refresh_history).unbind(1)); - global_history_checkbox->connect("toggled", callable_mp(this, &HistoryDock::save_options).unbind(1)); + global_history_checkbox->connect(SceneStringName(toggled), callable_mp(this, &HistoryDock::refresh_history).unbind(1)); + global_history_checkbox->connect(SceneStringName(toggled), callable_mp(this, &HistoryDock::save_options).unbind(1)); action_list = memnew(ItemList); action_list->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED); diff --git a/editor/icons/GuiSpinboxDown.svg b/editor/icons/GuiSpinboxDown.svg new file mode 100644 index 0000000000..f8f473ce1a --- /dev/null +++ b/editor/icons/GuiSpinboxDown.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="8"><path fill="none" stroke="#e0e0e0" stroke-linecap="round" stroke-linejoin="round" stroke-opacity=".8" stroke-width="2" d="m12 2-4 4-4-4"/></svg>
\ No newline at end of file diff --git a/editor/icons/GuiSpinboxUp.svg b/editor/icons/GuiSpinboxUp.svg new file mode 100644 index 0000000000..28bd0505d4 --- /dev/null +++ b/editor/icons/GuiSpinboxUp.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="8"><path fill="none" stroke="#e0e0e0" stroke-linecap="round" stroke-linejoin="round" stroke-opacity=".8" stroke-width="2" d="m4 6 4-4 4 4"/></svg>
\ No newline at end of file diff --git a/editor/icons/MemberConstructor.svg b/editor/icons/MemberConstructor.svg new file mode 100644 index 0000000000..0e61768739 --- /dev/null +++ b/editor/icons/MemberConstructor.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path fill="#e0e0e0" d="M9 6c-1.33 2.67-1.33 4.33 0 7h2c-1.33-2.67-1.33-4.33 0-7zm4 0c1.33 2.67 1.33 4.33 0 7h2c1.33-2.67 1.33-4.33 0-7zM1 11a1 1 0 0 0 0 2 1 1 0 0 0 0-2zm6 2v-2H6a1 1 0 0 1 0-2h1V7H6a3 3 0 0 0 0 6z"/></svg>
\ No newline at end of file diff --git a/editor/icons/MemberOperator.svg b/editor/icons/MemberOperator.svg new file mode 100644 index 0000000000..a00d990e26 --- /dev/null +++ b/editor/icons/MemberOperator.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path fill="#e0e0e0" d="M9 6c-1.33 2.67-1.33 4.33 0 7h2c-1.33-2.67-1.33-4.33 0-7zm4 0c1.33 2.67 1.33 4.33 0 7h2c1.33-2.67 1.33-4.33 0-7zM1 11a1 1 0 0 0 0 2 1 1 0 0 0 0-2zm4-4a3 3 0 0 0 0 6 3 3 0 0 0 0-6zm0 2a1 1 0 0 1 0 2 1 1 0 0 1 0-2z"/></svg>
\ No newline at end of file diff --git a/editor/icons/SnapKeys.svg b/editor/icons/SnapKeys.svg new file mode 100644 index 0000000000..813781b801 --- /dev/null +++ b/editor/icons/SnapKeys.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path fill="#e0e0e0" d="m 7,13 v 2 h 2 v -2 z m 6,0 v 2 h 2 v -2 z"/><path fill="#fff" fill-opacity=".686" d="m 7,13 h 2 v -2 a 2,2 0 0 1 4,0 v 2 h 2 v -2 a 4,4 0 0 0 -8,0 z"/><path fill="#fff" d="M 5,1 1,5 5,9 9,5 Z"/></svg>
\ No newline at end of file diff --git a/editor/icons/SnapTimeline.svg b/editor/icons/SnapTimeline.svg new file mode 100644 index 0000000000..b558545f5a --- /dev/null +++ b/editor/icons/SnapTimeline.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path fill="#e0e0e0" d="m 7,13 v 2 h 2 v -2 z m 6,0 v 2 h 2 v -2 z"/><path fill="#fff" fill-opacity=".686" d="m 7,13 h 2 v -2 a 2,2 0 0 1 4,0 v 2 h 2 v -2 a 4,4 0 0 0 -8,0 z"/><path fill="#fff" d="m 1,2 3,3 v 9 H 6 V 5 L 9,2 Z"/></svg>
\ No newline at end of file diff --git a/editor/icons/editor_icons_builders.py b/editor/icons/editor_icons_builders.py index 5cc67ca3ad..d3e8953483 100644 --- a/editor/icons/editor_icons_builders.py +++ b/editor/icons/editor_icons_builders.py @@ -3,6 +3,8 @@ import os from io import StringIO +from methods import to_raw_cstring + # See also `scene/theme/icons/default_theme_icons_builders.py`. def make_editor_icons_action(target, source, env): @@ -10,21 +12,9 @@ def make_editor_icons_action(target, source, env): svg_icons = source with StringIO() as icons_string, StringIO() as s: - for f in svg_icons: - fname = str(f) - - icons_string.write('\t"') - - with open(fname, "rb") as svgf: - b = svgf.read(1) - while len(b) == 1: - icons_string.write("\\" + str(hex(ord(b)))[1:]) - b = svgf.read(1) - - icons_string.write('"') - if fname != svg_icons[-1]: - icons_string.write(",") - icons_string.write("\n") + for svg in svg_icons: + with open(str(svg), "r") as svgf: + icons_string.write("\t%s,\n" % to_raw_cstring(svgf.read())) s.write("/* THIS FILE IS GENERATED DO NOT EDIT */\n") s.write("#ifndef _EDITOR_ICONS_H\n") diff --git a/editor/import/3d/post_import_plugin_skeleton_rest_fixer.cpp b/editor/import/3d/post_import_plugin_skeleton_rest_fixer.cpp index b69d38afa0..64bec0532b 100644 --- a/editor/import/3d/post_import_plugin_skeleton_rest_fixer.cpp +++ b/editor/import/3d/post_import_plugin_skeleton_rest_fixer.cpp @@ -48,13 +48,13 @@ void PostImportPluginSkeletonRestFixer::get_internal_import_options(InternalImpo // TODO: PostImportPlugin need to be implemented such as validate_option(PropertyInfo &property, const Dictionary &p_options). // get_internal_option_visibility() is not sufficient because it can only retrieve options implemented in the core and can only read option values. // r_options->push_back(ResourceImporter::ImportOption(PropertyInfo(Variant::ARRAY, "retarget/rest_fixer/filter", PROPERTY_HINT_ARRAY_TYPE, vformat("%s/%s:%s", Variant::STRING_NAME, PROPERTY_HINT_ENUM, "Hips,Spine,Chest")), Array())); - r_options->push_back(ResourceImporter::ImportOption(PropertyInfo(Variant::ARRAY, "retarget/rest_fixer/fix_silhouette/filter", PROPERTY_HINT_ARRAY_TYPE, "StringName"), Array())); + r_options->push_back(ResourceImporter::ImportOption(PropertyInfo(Variant::ARRAY, "retarget/rest_fixer/fix_silhouette/filter", PROPERTY_HINT_ARRAY_TYPE, vformat("%s:", Variant::STRING_NAME)), Array())); r_options->push_back(ResourceImporter::ImportOption(PropertyInfo(Variant::FLOAT, "retarget/rest_fixer/fix_silhouette/threshold"), 15)); r_options->push_back(ResourceImporter::ImportOption(PropertyInfo(Variant::FLOAT, "retarget/rest_fixer/fix_silhouette/base_height_adjustment", PROPERTY_HINT_RANGE, "-1,1,0.01"), 0.0)); } } -Variant PostImportPluginSkeletonRestFixer::get_internal_option_visibility(InternalImportCategory p_category, bool p_for_animation, const String &p_option, const HashMap<StringName, Variant> &p_options) const { +Variant PostImportPluginSkeletonRestFixer::get_internal_option_visibility(InternalImportCategory p_category, const String &p_scene_import_type, const String &p_option, const HashMap<StringName, Variant> &p_options) const { if (p_category == INTERNAL_IMPORT_CATEGORY_SKELETON_3D_NODE) { if (p_option.begins_with("retarget/rest_fixer/fix_silhouette/")) { if (!bool(p_options["retarget/rest_fixer/fix_silhouette/enable"])) { diff --git a/editor/import/3d/post_import_plugin_skeleton_rest_fixer.h b/editor/import/3d/post_import_plugin_skeleton_rest_fixer.h index 1750ed1233..9fec76be48 100644 --- a/editor/import/3d/post_import_plugin_skeleton_rest_fixer.h +++ b/editor/import/3d/post_import_plugin_skeleton_rest_fixer.h @@ -38,7 +38,7 @@ class PostImportPluginSkeletonRestFixer : public EditorScenePostImportPlugin { public: virtual void get_internal_import_options(InternalImportCategory p_category, List<ResourceImporter::ImportOption> *r_options) override; - virtual Variant get_internal_option_visibility(InternalImportCategory p_category, bool p_for_animation, const String &p_option, const HashMap<StringName, Variant> &p_options) const override; + virtual Variant get_internal_option_visibility(InternalImportCategory p_category, const String &p_scene_import_type, const String &p_option, const HashMap<StringName, Variant> &p_options) const override; virtual void internal_process(InternalImportCategory p_category, Node *p_base_scene, Node *p_node, Ref<Resource> p_resource, const Dictionary &p_options) override; PostImportPluginSkeletonRestFixer(); diff --git a/editor/import/3d/resource_importer_scene.cpp b/editor/import/3d/resource_importer_scene.cpp index e259181187..fa07511dd0 100644 --- a/editor/import/3d/resource_importer_scene.cpp +++ b/editor/import/3d/resource_importer_scene.cpp @@ -96,9 +96,10 @@ void EditorSceneFormatImporter::get_import_options(const String &p_path, List<Re GDVIRTUAL_CALL(_get_import_options, p_path); } -Variant EditorSceneFormatImporter::get_option_visibility(const String &p_path, bool p_for_animation, const String &p_option, const HashMap<StringName, Variant> &p_options) { +Variant EditorSceneFormatImporter::get_option_visibility(const String &p_path, const String &p_scene_import_type, const String &p_option, const HashMap<StringName, Variant> &p_options) { Variant ret; - GDVIRTUAL_CALL(_get_option_visibility, p_path, p_for_animation, p_option, ret); + // For compatibility with the old API, pass the import type as a boolean. + GDVIRTUAL_CALL(_get_option_visibility, p_path, p_scene_import_type == "AnimationLibrary", p_option, ret); return ret; } @@ -172,13 +173,16 @@ void EditorScenePostImportPlugin::get_internal_import_options(InternalImportCate GDVIRTUAL_CALL(_get_internal_import_options, p_category); current_option_list = nullptr; } -Variant EditorScenePostImportPlugin::get_internal_option_visibility(InternalImportCategory p_category, bool p_for_animation, const String &p_option, const HashMap<StringName, Variant> &p_options) const { + +Variant EditorScenePostImportPlugin::get_internal_option_visibility(InternalImportCategory p_category, const String &p_scene_import_type, const String &p_option, const HashMap<StringName, Variant> &p_options) const { current_options = &p_options; Variant ret; - GDVIRTUAL_CALL(_get_internal_option_visibility, p_category, p_for_animation, p_option, ret); + // For compatibility with the old API, pass the import type as a boolean. + GDVIRTUAL_CALL(_get_internal_option_visibility, p_category, p_scene_import_type == "AnimationLibrary", p_option, ret); current_options = nullptr; return ret; } + Variant EditorScenePostImportPlugin::get_internal_option_update_view_required(InternalImportCategory p_category, const String &p_option, const HashMap<StringName, Variant> &p_options) const { current_options = &p_options; Variant ret; @@ -198,10 +202,10 @@ void EditorScenePostImportPlugin::get_import_options(const String &p_path, List< GDVIRTUAL_CALL(_get_import_options, p_path); current_option_list = nullptr; } -Variant EditorScenePostImportPlugin::get_option_visibility(const String &p_path, bool p_for_animation, const String &p_option, const HashMap<StringName, Variant> &p_options) const { +Variant EditorScenePostImportPlugin::get_option_visibility(const String &p_path, const String &p_scene_import_type, const String &p_option, const HashMap<StringName, Variant> &p_options) const { current_options = &p_options; Variant ret; - GDVIRTUAL_CALL(_get_option_visibility, p_path, p_for_animation, p_option, ret); + GDVIRTUAL_CALL(_get_option_visibility, p_path, p_scene_import_type == "AnimationLibrary", p_option, ret); current_options = nullptr; return ret; } @@ -245,11 +249,22 @@ void EditorScenePostImportPlugin::_bind_methods() { ///////////////////////////////////////////////////////// String ResourceImporterScene::get_importer_name() const { - return animation_importer ? "animation_library" : "scene"; + // For compatibility with 4.2 and earlier we need to keep the "scene" and "animation_library" names. + // However this is arbitrary so for new import types we can use any string. + if (_scene_import_type == "PackedScene") { + return "scene"; + } else if (_scene_import_type == "AnimationLibrary") { + return "animation_library"; + } + return _scene_import_type; } String ResourceImporterScene::get_visible_name() const { - return animation_importer ? "Animation Library" : "Scene"; + // This is displayed on the UI. Friendly names here are nice but not vital, so fall back to the type. + if (_scene_import_type == "PackedScene") { + return "Scene"; + } + return _scene_import_type.capitalize(); } void ResourceImporterScene::get_recognized_extensions(List<String> *p_extensions) const { @@ -257,11 +272,14 @@ void ResourceImporterScene::get_recognized_extensions(List<String> *p_extensions } String ResourceImporterScene::get_save_extension() const { - return animation_importer ? "res" : "scn"; + if (_scene_import_type == "PackedScene") { + return "scn"; + } + return "res"; } String ResourceImporterScene::get_resource_type() const { - return animation_importer ? "AnimationLibrary" : "PackedScene"; + return _scene_import_type; } int ResourceImporterScene::get_format_version() const { @@ -269,27 +287,28 @@ int ResourceImporterScene::get_format_version() const { } bool ResourceImporterScene::get_option_visibility(const String &p_path, const String &p_option, const HashMap<StringName, Variant> &p_options) const { - if (animation_importer) { + if (_scene_import_type == "PackedScene") { + if (p_option.begins_with("animation/")) { + if (p_option != "animation/import" && !bool(p_options["animation/import"])) { + return false; + } + } + } else if (_scene_import_type == "AnimationLibrary") { if (p_option == "animation/import") { // Option ignored, animation always imported. return false; } - } else if (p_option.begins_with("animation/")) { - if (p_option != "animation/import" && !bool(p_options["animation/import"])) { - return false; + if (p_option == "nodes/root_type" || p_option == "nodes/root_name" || p_option.begins_with("meshes/") || p_option.begins_with("skins/")) { + return false; // Nothing to do here for animations. } } - if (animation_importer && (p_option == "nodes/root_type" || p_option == "nodes/root_name" || p_option.begins_with("meshes/") || p_option.begins_with("skins/"))) { - return false; // Nothing to do here for animations. - } - if (p_option == "meshes/lightmap_texel_size" && int(p_options["meshes/light_baking"]) != 2) { // Only display the lightmap texel size import option when using the Static Lightmaps light baking mode. return false; } for (int i = 0; i < post_importer_plugins.size(); i++) { - Variant ret = post_importer_plugins.write[i]->get_option_visibility(p_path, animation_importer, p_option, p_options); + Variant ret = post_importer_plugins.write[i]->get_option_visibility(p_path, _scene_import_type, p_option, p_options); if (ret.get_type() == Variant::BOOL) { if (!ret) { return false; @@ -298,7 +317,7 @@ bool ResourceImporterScene::get_option_visibility(const String &p_path, const St } for (Ref<EditorSceneFormatImporter> importer : scene_importers) { - Variant ret = importer->get_option_visibility(p_path, animation_importer, p_option, p_options); + Variant ret = importer->get_option_visibility(p_path, _scene_import_type, p_option, p_options); if (ret.get_type() == Variant::BOOL) { if (!ret) { return false; @@ -630,6 +649,9 @@ Node *ResourceImporterScene::_pre_fix_node(Node *p_node, Node *p_root, HashMap<R String name = p_node->get_name(); NodePath original_path = p_root->get_path_to(p_node); // Used to detect renames due to import hints. + Ref<Resource> original_meta = memnew(Resource); // Create temp resource to hold original meta + original_meta->merge_meta_from(p_node); + bool isroot = p_node == p_root; if (!isroot && _teststr(name, "noimp")) { @@ -1003,6 +1025,8 @@ Node *ResourceImporterScene::_pre_fix_node(Node *p_node, Node *p_root, HashMap<R print_verbose(vformat("Fix: Renamed %s to %s", original_path, new_path)); r_node_renames.push_back({ original_path, p_node }); } + // If we created new node instead, merge meta values from the original node. + p_node->merge_meta_from(*original_meta); } return p_node; @@ -2283,7 +2307,7 @@ bool ResourceImporterScene::get_internal_option_visibility(InternalImportCategor } for (int i = 0; i < post_importer_plugins.size(); i++) { - Variant ret = post_importer_plugins.write[i]->get_internal_option_visibility(EditorScenePostImportPlugin::InternalImportCategory(p_category), animation_importer, p_option, p_options); + Variant ret = post_importer_plugins.write[i]->get_internal_option_visibility(EditorScenePostImportPlugin::InternalImportCategory(p_category), _scene_import_type, p_option, p_options); if (ret.get_type() == Variant::BOOL) { return ret; } @@ -2433,6 +2457,8 @@ Node *ResourceImporterScene::_generate_meshes(Node *p_node, const Dictionary &p_ mesh_node->set_transform(src_mesh_node->get_transform()); mesh_node->set_skin(src_mesh_node->get_skin()); mesh_node->set_skeleton_path(src_mesh_node->get_skeleton_path()); + mesh_node->merge_meta_from(src_mesh_node); + if (src_mesh_node->get_mesh().is_valid()) { Ref<ArrayMesh> mesh; if (!src_mesh_node->get_mesh()->has_mesh()) { @@ -2580,6 +2606,7 @@ Node *ResourceImporterScene::_generate_meshes(Node *p_node, const Dictionary &p_ for (int i = 0; i < mesh->get_surface_count(); i++) { mesh_node->set_surface_override_material(i, src_mesh_node->get_surface_material(i)); } + mesh->merge_meta_from(*src_mesh_node->get_mesh()); } } @@ -2877,13 +2904,11 @@ Error ResourceImporterScene::import(const String &p_source_file, const String &p int import_flags = 0; - if (animation_importer) { + if (_scene_import_type == "AnimationLibrary") { import_flags |= EditorSceneFormatImporter::IMPORT_ANIMATION; import_flags |= EditorSceneFormatImporter::IMPORT_DISCARD_MESHES_AND_MATERIALS; - } else { - if (bool(p_options["animation/import"])) { - import_flags |= EditorSceneFormatImporter::IMPORT_ANIMATION; - } + } else if (bool(p_options["animation/import"])) { + import_flags |= EditorSceneFormatImporter::IMPORT_ANIMATION; } if (bool(p_options["skins/use_named_skins"])) { @@ -3058,13 +3083,13 @@ Error ResourceImporterScene::import(const String &p_source_file, const String &p progress.step(TTR("Running Custom Script..."), 2); String post_import_script_path = p_options["import_script/path"]; - if (post_import_script_path.is_relative_path()) { - post_import_script_path = p_source_file.get_base_dir().path_join(post_import_script_path); - } Ref<EditorScenePostImport> post_import_script; if (!post_import_script_path.is_empty()) { + if (post_import_script_path.is_relative_path()) { + post_import_script_path = p_source_file.get_base_dir().path_join(post_import_script_path); + } Ref<Script> scr = ResourceLoader::load(post_import_script_path); if (!scr.is_valid()) { EditorNode::add_io_error(TTR("Couldn't load post-import script:") + " " + post_import_script_path); @@ -3079,6 +3104,19 @@ Error ResourceImporterScene::import(const String &p_source_file, const String &p } } + // Apply RESET animation before serializing. + if (_scene_import_type == "PackedScene") { + int scene_child_count = scene->get_child_count(); + for (int i = 0; i < scene_child_count; i++) { + AnimationPlayer *ap = Object::cast_to<AnimationPlayer>(scene->get_child(i)); + if (ap) { + if (ap->can_apply_reset()) { + ap->apply_reset(); + } + } + } + } + if (post_import_script.is_valid()) { post_import_script->init(p_source_file); scene = post_import_script->post_import(scene); @@ -3097,11 +3135,11 @@ Error ResourceImporterScene::import(const String &p_source_file, const String &p progress.step(TTR("Saving..."), 104); int flags = 0; - if (EDITOR_GET("filesystem/on_save/compress_binary_resources")) { + if (EditorSettings::get_singleton() && EDITOR_GET("filesystem/on_save/compress_binary_resources")) { flags |= ResourceSaver::FLAG_COMPRESS; } - if (animation_importer) { + if (_scene_import_type == "AnimationLibrary") { Ref<AnimationLibrary> library; for (int i = 0; i < scene->get_child_count(); i++) { AnimationPlayer *ap = Object::cast_to<AnimationPlayer>(scene->get_child(i)); @@ -3122,13 +3160,14 @@ Error ResourceImporterScene::import(const String &p_source_file, const String &p print_verbose("Saving animation to: " + p_save_path + ".res"); err = ResourceSaver::save(library, p_save_path + ".res", flags); //do not take over, let the changed files reload themselves ERR_FAIL_COND_V_MSG(err != OK, err, "Cannot save animation to file '" + p_save_path + ".res'."); - - } else { + } else if (_scene_import_type == "PackedScene") { Ref<PackedScene> packer = memnew(PackedScene); packer->pack(scene); print_verbose("Saving scene to: " + p_save_path + ".scn"); err = ResourceSaver::save(packer, p_save_path + ".scn", flags); //do not take over, let the changed files reload themselves ERR_FAIL_COND_V_MSG(err != OK, err, "Cannot save scene to file '" + p_save_path + ".scn'."); + } else { + ERR_FAIL_V_MSG(ERR_FILE_UNRECOGNIZED, "Unknown scene import type: " + _scene_import_type); } memdelete(scene); @@ -3150,20 +3189,20 @@ bool ResourceImporterScene::has_advanced_options() const { } void ResourceImporterScene::show_advanced_options(const String &p_path) { - SceneImportSettingsDialog::get_singleton()->open_settings(p_path, animation_importer); + SceneImportSettingsDialog::get_singleton()->open_settings(p_path, _scene_import_type); } -ResourceImporterScene::ResourceImporterScene(bool p_animation_import, bool p_singleton) { +ResourceImporterScene::ResourceImporterScene(const String &p_scene_import_type, bool p_singleton) { // This should only be set through the EditorNode. if (p_singleton) { - if (p_animation_import) { + if (p_scene_import_type == "AnimationLibrary") { animation_singleton = this; - } else { + } else if (p_scene_import_type == "PackedScene") { scene_singleton = this; } } - animation_importer = p_animation_import; + _scene_import_type = p_scene_import_type; } ResourceImporterScene::~ResourceImporterScene() { diff --git a/editor/import/3d/resource_importer_scene.h b/editor/import/3d/resource_importer_scene.h index f9124ad289..9759f328d7 100644 --- a/editor/import/3d/resource_importer_scene.h +++ b/editor/import/3d/resource_importer_scene.h @@ -44,10 +44,10 @@ #include "scene/resources/animation.h" #include "scene/resources/mesh.h" -class Material; class AnimationPlayer; - class ImporterMesh; +class Material; + class EditorSceneFormatImporter : public RefCounted { GDCLASS(EditorSceneFormatImporter, RefCounted); @@ -78,7 +78,7 @@ public: virtual void get_extensions(List<String> *r_extensions) const; virtual Node *import_scene(const String &p_path, uint32_t p_flags, const HashMap<StringName, Variant> &p_options, List<String> *r_missing_deps, Error *r_err = nullptr); virtual void get_import_options(const String &p_path, List<ResourceImporter::ImportOption> *r_options); - virtual Variant get_option_visibility(const String &p_path, bool p_for_animation, const String &p_option, const HashMap<StringName, Variant> &p_options); + virtual Variant get_option_visibility(const String &p_path, const String &p_scene_import_type, const String &p_option, const HashMap<StringName, Variant> &p_options); virtual void handle_compatibility_options(HashMap<StringName, Variant> &p_import_params) const {} EditorSceneFormatImporter() {} @@ -140,13 +140,13 @@ public: void add_import_option_advanced(Variant::Type p_type, const String &p_name, const Variant &p_default_value, PropertyHint p_hint = PROPERTY_HINT_NONE, const String &p_hint_string = String(), int p_usage_flags = PROPERTY_USAGE_DEFAULT); virtual void get_internal_import_options(InternalImportCategory p_category, List<ResourceImporter::ImportOption> *r_options); - virtual Variant get_internal_option_visibility(InternalImportCategory p_category, bool p_for_animation, const String &p_option, const HashMap<StringName, Variant> &p_options) const; + virtual Variant get_internal_option_visibility(InternalImportCategory p_category, const String &p_scene_import_type, const String &p_option, const HashMap<StringName, Variant> &p_options) const; virtual Variant get_internal_option_update_view_required(InternalImportCategory p_category, const String &p_option, const HashMap<StringName, Variant> &p_options) const; virtual void internal_process(InternalImportCategory p_category, Node *p_base_scene, Node *p_node, Ref<Resource> p_resource, const Dictionary &p_options); virtual void get_import_options(const String &p_path, List<ResourceImporter::ImportOption> *r_options); - virtual Variant get_option_visibility(const String &p_path, bool p_for_animation, const String &p_option, const HashMap<StringName, Variant> &p_options) const; + virtual Variant get_option_visibility(const String &p_path, const String &p_scene_import_type, const String &p_option, const HashMap<StringName, Variant> &p_options) const; virtual void pre_process(Node *p_scene, const HashMap<StringName, Variant> &p_options); virtual void post_process(Node *p_scene, const HashMap<StringName, Variant> &p_options); @@ -237,7 +237,7 @@ class ResourceImporterScene : public ResourceImporter { void _optimize_track_usage(AnimationPlayer *p_player, AnimationImportTracks *p_track_actions); - bool animation_importer = false; + String _scene_import_type = "PackedScene"; public: static ResourceImporterScene *get_scene_singleton() { return scene_singleton; } @@ -253,6 +253,9 @@ public: static void clean_up_importer_plugins(); + String get_scene_import_type() const { return _scene_import_type; } + void set_scene_import_type(const String &p_type) { _scene_import_type = p_type; } + virtual String get_importer_name() const override; virtual String get_visible_name() const override; virtual void get_recognized_extensions(List<String> *p_extensions) const override; @@ -303,7 +306,7 @@ public: virtual bool can_import_threaded() const override { return false; } - ResourceImporterScene(bool p_animation_import = false, bool p_singleton = false); + ResourceImporterScene(const String &p_scene_import_type = "PackedScene", bool p_singleton = false); ~ResourceImporterScene(); template <typename M> diff --git a/editor/import/3d/scene_import_settings.cpp b/editor/import/3d/scene_import_settings.cpp index e131794e84..7ca3cb6c3a 100644 --- a/editor/import/3d/scene_import_settings.cpp +++ b/editor/import/3d/scene_import_settings.cpp @@ -464,7 +464,14 @@ void SceneImportSettingsDialog::_fill_scene(Node *p_node, TreeItem *p_parent_ite mesh_node->add_child(collider_view, true); collider_view->set_owner(mesh_node); - AABB aabb = mesh_node->get_aabb(); + Transform3D accum_xform; + Node3D *base = mesh_node; + while (base) { + accum_xform = base->get_transform() * accum_xform; + base = Object::cast_to<Node3D>(base->get_parent()); + } + + AABB aabb = accum_xform.xform(mesh_node->get_mesh()->get_aabb()); if (first_aabb) { contents_aabb = aabb; @@ -671,26 +678,26 @@ void SceneImportSettingsDialog::update_view() { update_view_timer->start(); } -void SceneImportSettingsDialog::open_settings(const String &p_path, bool p_for_animation) { +void SceneImportSettingsDialog::open_settings(const String &p_path, const String &p_scene_import_type) { if (scene) { _cleanup(); memdelete(scene); scene = nullptr; } - editing_animation = p_for_animation; + editing_animation = p_scene_import_type == "AnimationLibrary"; scene_import_settings_data->settings = nullptr; scene_import_settings_data->path = p_path; // Visibility. - data_mode->set_tab_hidden(1, p_for_animation); - data_mode->set_tab_hidden(2, p_for_animation); - if (p_for_animation) { + data_mode->set_tab_hidden(1, editing_animation); + data_mode->set_tab_hidden(2, editing_animation); + if (editing_animation) { data_mode->set_current_tab(0); } - action_menu->get_popup()->set_item_disabled(action_menu->get_popup()->get_item_id(ACTION_EXTRACT_MATERIALS), p_for_animation); - action_menu->get_popup()->set_item_disabled(action_menu->get_popup()->get_item_id(ACTION_CHOOSE_MESH_SAVE_PATHS), p_for_animation); + action_menu->get_popup()->set_item_disabled(action_menu->get_popup()->get_item_id(ACTION_EXTRACT_MATERIALS), editing_animation); + action_menu->get_popup()->set_item_disabled(action_menu->get_popup()->get_item_id(ACTION_CHOOSE_MESH_SAVE_PATHS), editing_animation); base_path = p_path; @@ -764,7 +771,7 @@ void SceneImportSettingsDialog::open_settings(const String &p_path, bool p_for_a // Start with the root item (Scene) selected. scene_tree->get_root()->select(0); - if (p_for_animation) { + if (editing_animation) { set_title(vformat(TTR("Advanced Import Settings for AnimationLibrary '%s'"), base_path.get_file())); } else { set_title(vformat(TTR("Advanced Import Settings for Scene '%s'"), base_path.get_file())); diff --git a/editor/import/3d/scene_import_settings.h b/editor/import/3d/scene_import_settings.h index c2a5151432..bbd0d2c22d 100644 --- a/editor/import/3d/scene_import_settings.h +++ b/editor/import/3d/scene_import_settings.h @@ -243,7 +243,7 @@ public: bool is_editing_animation() const { return editing_animation; } void request_generate_collider(); void update_view(); - void open_settings(const String &p_path, bool p_for_animation = false); + void open_settings(const String &p_path, const String &p_scene_import_type = "PackedScene"); static SceneImportSettingsDialog *get_singleton(); Node *get_selected_node(); SceneImportSettingsDialog(); diff --git a/editor/import/audio_stream_import_settings.cpp b/editor/import/audio_stream_import_settings.cpp index a53deefee9..9a0c62193c 100644 --- a/editor/import/audio_stream_import_settings.cpp +++ b/editor/import/audio_stream_import_settings.cpp @@ -537,7 +537,7 @@ AudioStreamImportSettingsDialog::AudioStreamImportSettingsDialog() { loop = memnew(CheckBox); loop->set_text(TTR("Enable")); loop->set_tooltip_text(TTR("Enable looping.")); - loop->connect("toggled", callable_mp(this, &AudioStreamImportSettingsDialog::_settings_changed).unbind(1)); + loop->connect(SceneStringName(toggled), callable_mp(this, &AudioStreamImportSettingsDialog::_settings_changed).unbind(1)); loop_hb->add_child(loop); loop_hb->add_spacer(); loop_hb->add_child(memnew(Label(TTR("Offset:")))); @@ -554,7 +554,7 @@ AudioStreamImportSettingsDialog::AudioStreamImportSettingsDialog() { interactive_hb->add_theme_constant_override("separation", 4 * EDSCALE); bpm_enabled = memnew(CheckBox); bpm_enabled->set_text((TTR("BPM:"))); - bpm_enabled->connect("toggled", callable_mp(this, &AudioStreamImportSettingsDialog::_settings_changed).unbind(1)); + bpm_enabled->connect(SceneStringName(toggled), callable_mp(this, &AudioStreamImportSettingsDialog::_settings_changed).unbind(1)); interactive_hb->add_child(bpm_enabled); bpm_edit = memnew(SpinBox); bpm_edit->set_max(400); @@ -565,7 +565,7 @@ AudioStreamImportSettingsDialog::AudioStreamImportSettingsDialog() { interactive_hb->add_spacer(); beats_enabled = memnew(CheckBox); beats_enabled->set_text(TTR("Beat Count:")); - beats_enabled->connect("toggled", callable_mp(this, &AudioStreamImportSettingsDialog::_settings_changed).unbind(1)); + beats_enabled->connect(SceneStringName(toggled), callable_mp(this, &AudioStreamImportSettingsDialog::_settings_changed).unbind(1)); interactive_hb->add_child(beats_enabled); beats_edit = memnew(SpinBox); beats_edit->set_tooltip_text(TTR("Configure the amount of Beats used for music-aware looping. If zero, it will be autodetected from the length.\nIt is recommended to set this value (either manually or by clicking on a beat number in the preview) to ensure looping works properly.")); diff --git a/editor/import/resource_importer_wav.cpp b/editor/import/resource_importer_wav.cpp index 6d3d474cee..77b3629b07 100644 --- a/editor/import/resource_importer_wav.cpp +++ b/editor/import/resource_importer_wav.cpp @@ -90,7 +90,8 @@ void ResourceImporterWAV::get_import_options(const String &p_path, List<ImportOp r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "edit/loop_mode", PROPERTY_HINT_ENUM, "Detect From WAV,Disabled,Forward,Ping-Pong,Backward", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED), 0)); r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "edit/loop_begin"), 0)); r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "edit/loop_end"), -1)); - r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "compress/mode", PROPERTY_HINT_ENUM, "Disabled,RAM (Ima-ADPCM),QOA (Quite OK Audio)"), 0)); + // Quite OK Audio is lightweight enough and supports virtually every significant AudioStreamWAV feature. + r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "compress/mode", PROPERTY_HINT_ENUM, "PCM (Uncompressed),IMA ADPCM,Quite OK Audio"), 2)); } Error ResourceImporterWAV::import(const String &p_source_file, const String &p_save_path, const HashMap<StringName, Variant> &p_options, List<String> *r_platform_variants, List<String> *r_gen_files, Variant *r_metadata) { @@ -517,16 +518,19 @@ Error ResourceImporterWAV::import(const String &p_source_file, const String &p_s Vector<uint8_t> dst_data; if (compression == 2) { dst_format = AudioStreamWAV::FORMAT_QOA; - qoa_desc desc = { 0, 0, 0, { { { 0 }, { 0 } } } }; + qoa_desc desc = {}; uint32_t qoa_len = 0; desc.samplerate = rate; desc.samples = frames; desc.channels = format_channels; - void *encoded = qoa_encode((short *)pcm_data.ptrw(), &desc, &qoa_len); - dst_data.resize(qoa_len); - memcpy(dst_data.ptrw(), encoded, qoa_len); + void *encoded = qoa_encode((short *)pcm_data.ptr(), &desc, &qoa_len); + if (encoded) { + dst_data.resize(qoa_len); + memcpy(dst_data.ptrw(), encoded, qoa_len); + QOA_FREE(encoded); + } } else { dst_data = pcm_data; } diff --git a/editor/input_event_configuration_dialog.cpp b/editor/input_event_configuration_dialog.cpp index dc839b02f6..c60197b96b 100644 --- a/editor/input_event_configuration_dialog.cpp +++ b/editor/input_event_configuration_dialog.cpp @@ -720,7 +720,7 @@ InputEventConfigurationDialog::InputEventConfigurationDialog() { for (int i = 0; i < MOD_MAX; i++) { String name = mods[i]; mod_checkboxes[i] = memnew(CheckBox); - mod_checkboxes[i]->connect("toggled", callable_mp(this, &InputEventConfigurationDialog::_mod_toggled).bind(i)); + mod_checkboxes[i]->connect(SceneStringName(toggled), callable_mp(this, &InputEventConfigurationDialog::_mod_toggled).bind(i)); mod_checkboxes[i]->set_text(name); mod_checkboxes[i]->set_tooltip_text(TTR(mods_tip[i])); mod_container->add_child(mod_checkboxes[i]); @@ -729,7 +729,7 @@ InputEventConfigurationDialog::InputEventConfigurationDialog() { mod_container->add_child(memnew(VSeparator)); autoremap_command_or_control_checkbox = memnew(CheckBox); - autoremap_command_or_control_checkbox->connect("toggled", callable_mp(this, &InputEventConfigurationDialog::_autoremap_command_or_control_toggled)); + autoremap_command_or_control_checkbox->connect(SceneStringName(toggled), callable_mp(this, &InputEventConfigurationDialog::_autoremap_command_or_control_toggled)); autoremap_command_or_control_checkbox->set_pressed(false); autoremap_command_or_control_checkbox->set_text(TTR("Command / Control (auto)")); autoremap_command_or_control_checkbox->set_tooltip_text(TTR("Automatically remaps between 'Meta' ('Command') and 'Control' depending on current platform.")); diff --git a/editor/inspector_dock.cpp b/editor/inspector_dock.cpp index aa075c80c3..dc07403213 100644 --- a/editor/inspector_dock.cpp +++ b/editor/inspector_dock.cpp @@ -190,7 +190,7 @@ void InspectorDock::_menu_option_confirm(int p_option, bool p_confirmed) { } int history_id = EditorUndoRedoManager::get_singleton()->get_history_id_for_object(current); - EditorUndoRedoManager::get_singleton()->clear_history(true, history_id); + EditorUndoRedoManager::get_singleton()->clear_history(history_id); EditorNode::get_singleton()->edit_item(current, inspector); } diff --git a/editor/plugins/animation_blend_space_1d_editor.cpp b/editor/plugins/animation_blend_space_1d_editor.cpp index 7d580e8de9..cbf8b27b32 100644 --- a/editor/plugins/animation_blend_space_1d_editor.cpp +++ b/editor/plugins/animation_blend_space_1d_editor.cpp @@ -713,7 +713,7 @@ AnimationNodeBlendSpace1DEditor::AnimationNodeBlendSpace1DEditor() { top_hb->add_child(memnew(Label(TTR("Sync:")))); sync = memnew(CheckBox); top_hb->add_child(sync); - sync->connect("toggled", callable_mp(this, &AnimationNodeBlendSpace1DEditor::_config_changed)); + sync->connect(SceneStringName(toggled), callable_mp(this, &AnimationNodeBlendSpace1DEditor::_config_changed)); top_hb->add_child(memnew(VSeparator)); diff --git a/editor/plugins/animation_blend_space_2d_editor.cpp b/editor/plugins/animation_blend_space_2d_editor.cpp index 55949df54f..934f26415a 100644 --- a/editor/plugins/animation_blend_space_2d_editor.cpp +++ b/editor/plugins/animation_blend_space_2d_editor.cpp @@ -961,7 +961,7 @@ AnimationNodeBlendSpace2DEditor::AnimationNodeBlendSpace2DEditor() { top_hb->add_child(memnew(Label(TTR("Sync:")))); sync = memnew(CheckBox); top_hb->add_child(sync); - sync->connect("toggled", callable_mp(this, &AnimationNodeBlendSpace2DEditor::_config_changed)); + sync->connect(SceneStringName(toggled), callable_mp(this, &AnimationNodeBlendSpace2DEditor::_config_changed)); top_hb->add_child(memnew(VSeparator)); diff --git a/editor/plugins/animation_library_editor.cpp b/editor/plugins/animation_library_editor.cpp index b07db993ba..38f8b16b34 100644 --- a/editor/plugins/animation_library_editor.cpp +++ b/editor/plugins/animation_library_editor.cpp @@ -561,7 +561,9 @@ void AnimationLibraryEditor::_button_pressed(TreeItem *p_item, int p_column, int return; } - anim = anim->duplicate(); // Users simply dont care about referencing, so making a copy works better here. + if (!anim->get_path().is_resource_file()) { + anim = anim->duplicate(); // Users simply dont care about referencing, so making a copy works better here. + } String base_name; if (anim->get_name() != "") { diff --git a/editor/plugins/animation_player_editor_plugin.cpp b/editor/plugins/animation_player_editor_plugin.cpp index 660e4647a1..b882112950 100644 --- a/editor/plugins/animation_player_editor_plugin.cpp +++ b/editor/plugins/animation_player_editor_plugin.cpp @@ -224,6 +224,69 @@ void AnimationPlayerEditor::_autoplay_pressed() { } } +void AnimationPlayerEditor::_go_to_nearest_keyframe(bool p_backward) { + if (_get_current().is_empty()) { + return; + } + + Ref<Animation> anim = player->get_animation(player->get_assigned_animation()); + + double current_time = player->get_current_animation_position(); + // Offset the time to avoid finding the same keyframe with Animation::track_find_key(). + double time_offset = MAX(CMP_EPSILON * 2, current_time * CMP_EPSILON * 2); + double current_time_offset = current_time + (p_backward ? -time_offset : time_offset); + + float nearest_key_time = p_backward ? 0 : anim->get_length(); + int track_count = anim->get_track_count(); + bool bezier_active = track_editor->is_bezier_editor_active(); + + Node *root = get_tree()->get_edited_scene_root(); + EditorSelection *selection = EditorNode::get_singleton()->get_editor_selection(); + + Vector<int> selected_tracks; + for (int i = 0; i < track_count; ++i) { + if (selection->is_selected(root->get_node_or_null(anim->track_get_path(i)))) { + selected_tracks.push_back(i); + } + } + + // Find the nearest keyframe in selection if the scene has selected nodes + // or the nearest keyframe in the entire animation otherwise. + if (selected_tracks.size() > 0) { + for (int track : selected_tracks) { + if (bezier_active && anim->track_get_type(track) != Animation::TYPE_BEZIER) { + continue; + } + int key = anim->track_find_key(track, current_time_offset, Animation::FIND_MODE_NEAREST, false, !p_backward); + if (key == -1) { + continue; + } + double key_time = anim->track_get_key_time(track, key); + if ((p_backward && key_time > nearest_key_time) || (!p_backward && key_time < nearest_key_time)) { + nearest_key_time = key_time; + } + } + } else { + for (int track = 0; track < track_count; ++track) { + if (bezier_active && anim->track_get_type(track) != Animation::TYPE_BEZIER) { + continue; + } + int key = anim->track_find_key(track, current_time_offset, Animation::FIND_MODE_NEAREST, false, !p_backward); + if (key == -1) { + continue; + } + double key_time = anim->track_get_key_time(track, key); + if ((p_backward && key_time > nearest_key_time) || (!p_backward && key_time < nearest_key_time)) { + nearest_key_time = key_time; + } + } + } + + player->seek_internal(nearest_key_time, true, true, true); + frame->set_value(nearest_key_time); + track_editor->set_anim_pos(nearest_key_time); +} + void AnimationPlayerEditor::_play_pressed() { String current = _get_current(); @@ -474,17 +537,12 @@ void AnimationPlayerEditor::_select_anim_by_name(const String &p_anim) { } float AnimationPlayerEditor::_get_editor_step() const { - // Returns the effective snapping value depending on snapping modifiers, or 0 if snapping is disabled. - if (track_editor->is_snap_enabled()) { - const String current = player->get_assigned_animation(); - const Ref<Animation> anim = player->get_animation(current); - ERR_FAIL_COND_V(!anim.is_valid(), 0.0); + const String current = player->get_assigned_animation(); + const Ref<Animation> anim = player->get_animation(current); + ERR_FAIL_COND_V(anim.is_null(), 0.0); - // Use more precise snapping when holding Shift - return Input::get_singleton()->is_key_pressed(Key::SHIFT) ? anim->get_step() * 0.25 : anim->get_step(); - } - - return 0.0f; + // Use more precise snapping when holding Shift + return Input::get_singleton()->is_key_pressed(Key::SHIFT) ? anim->get_step() * 0.25 : anim->get_step(); } void AnimationPlayerEditor::_animation_name_edited() { @@ -1290,7 +1348,7 @@ void AnimationPlayerEditor::_seek_value_changed(float p_value, bool p_timeline_o anim = player->get_animation(current); double pos = CLAMP((double)anim->get_length() * (p_value / frame->get_max()), 0, (double)anim->get_length()); - if (track_editor->is_snap_enabled()) { + if (track_editor->is_snap_timeline_enabled()) { pos = Math::snapped(pos, _get_editor_step()); } pos = CLAMP(pos, 0, (double)anim->get_length() - CMP_EPSILON2); // Hack: Avoid fposmod with LOOP_LINEAR. @@ -1408,11 +1466,17 @@ void AnimationPlayerEditor::_animation_key_editor_seek(float p_pos, bool p_timel } updating = true; - frame->set_value(Math::snapped(p_pos, _get_editor_step())); + frame->set_value(track_editor->is_snap_timeline_enabled() ? Math::snapped(p_pos, _get_editor_step()) : p_pos); updating = false; _seek_value_changed(p_pos, p_timeline_only); } +void AnimationPlayerEditor::_animation_update_key_frame() { + if (player) { + player->advance(0); + } +} + void AnimationPlayerEditor::_animation_tool_menu(int p_option) { String current = _get_current(); @@ -1505,30 +1569,28 @@ void AnimationPlayerEditor::shortcut_input(const Ref<InputEvent> &p_ev) { ERR_FAIL_COND(p_ev.is_null()); Ref<InputEventKey> k = p_ev; - if (is_visible_in_tree() && k.is_valid() && k->is_pressed() && !k->is_echo() && !k->is_alt_pressed() && !k->is_ctrl_pressed() && !k->is_meta_pressed()) { - switch (k->get_keycode()) { - case Key::A: { - if (!k->is_shift_pressed()) { - _play_bw_from_pressed(); - } else { - _play_bw_pressed(); - } - accept_event(); - } break; - case Key::S: { - _stop_pressed(); - accept_event(); - } break; - case Key::D: { - if (!k->is_shift_pressed()) { - _play_from_pressed(); - } else { - _play_pressed(); - } - accept_event(); - } break; - default: - break; + if (is_visible_in_tree() && k.is_valid() && k->is_pressed() && !k->is_echo()) { + if (ED_IS_SHORTCUT("animation_editor/stop_animation", p_ev)) { + _stop_pressed(); + accept_event(); + } else if (ED_IS_SHORTCUT("animation_editor/play_animation", p_ev)) { + _play_from_pressed(); + accept_event(); + } else if (ED_IS_SHORTCUT("animation_editor/play_animation_backwards", p_ev)) { + _play_bw_from_pressed(); + accept_event(); + } else if (ED_IS_SHORTCUT("animation_editor/play_animation_from_start", p_ev)) { + _play_pressed(); + accept_event(); + } else if (ED_IS_SHORTCUT("animation_editor/play_animation_from_end", p_ev)) { + _play_bw_pressed(); + accept_event(); + } else if (ED_IS_SHORTCUT("animation_editor/go_to_next_keyframe", p_ev)) { + _go_to_nearest_keyframe(false); + accept_event(); + } else if (ED_IS_SHORTCUT("animation_editor/go_to_previous_keyframe", p_ev)) { + _go_to_nearest_keyframe(true); + accept_event(); } } } @@ -1874,6 +1936,7 @@ bool AnimationPlayerEditor::_validate_tracks(const Ref<Animation> p_anim) { void AnimationPlayerEditor::_bind_methods() { // Needed for UndoRedo. ClassDB::bind_method(D_METHOD("_animation_player_changed"), &AnimationPlayerEditor::_animation_player_changed); + ClassDB::bind_method(D_METHOD("_animation_update_key_frame"), &AnimationPlayerEditor::_animation_update_key_frame); ClassDB::bind_method(D_METHOD("_start_onion_skinning"), &AnimationPlayerEditor::_start_onion_skinning); ClassDB::bind_method(D_METHOD("_stop_onion_skinning"), &AnimationPlayerEditor::_stop_onion_skinning); @@ -1902,27 +1965,27 @@ AnimationPlayerEditor::AnimationPlayerEditor(AnimationPlayerEditorPlugin *p_plug play_bw_from = memnew(Button); play_bw_from->set_theme_type_variation("FlatButton"); - play_bw_from->set_tooltip_text(TTR("Play selected animation backwards from current pos. (A)")); + play_bw_from->set_tooltip_text(TTR("Play Animation Backwards")); hb->add_child(play_bw_from); play_bw = memnew(Button); play_bw->set_theme_type_variation("FlatButton"); - play_bw->set_tooltip_text(TTR("Play selected animation backwards from end. (Shift+A)")); + play_bw->set_tooltip_text(TTR("Play Animation Backwards from End")); hb->add_child(play_bw); stop = memnew(Button); stop->set_theme_type_variation("FlatButton"); + stop->set_tooltip_text(TTR("Pause/Stop Animation")); hb->add_child(stop); - stop->set_tooltip_text(TTR("Pause/stop animation playback. (S)")); play = memnew(Button); play->set_theme_type_variation("FlatButton"); - play->set_tooltip_text(TTR("Play selected animation from start. (Shift+D)")); + play->set_tooltip_text(TTR("Play Animation from Start")); hb->add_child(play); play_from = memnew(Button); play_from->set_theme_type_variation("FlatButton"); - play_from->set_tooltip_text(TTR("Play selected animation from current pos. (D)")); + play_from->set_tooltip_text(TTR("Play Animation")); hb->add_child(play_from); frame = memnew(SpinBox); @@ -2138,6 +2201,14 @@ void fragment() { } )"); RS::get_singleton()->material_set_shader(onion.capture.material->get_rid(), onion.capture.shader->get_rid()); + + ED_SHORTCUT("animation_editor/stop_animation", TTR("Pause/Stop Animation"), Key::S); + ED_SHORTCUT("animation_editor/play_animation", TTR("Play Animation"), Key::D); + ED_SHORTCUT("animation_editor/play_animation_backwards", TTR("Play Animation Backwards"), Key::A); + ED_SHORTCUT("animation_editor/play_animation_from_start", TTR("Play Animation from Start"), KeyModifierMask::SHIFT + Key::D); + ED_SHORTCUT("animation_editor/play_animation_from_end", TTR("Play Animation Backwards from End"), KeyModifierMask::SHIFT + Key::A); + ED_SHORTCUT("animation_editor/go_to_next_keyframe", TTR("Go to Next Keyframe"), KeyModifierMask::SHIFT + KeyModifierMask::ALT + Key::D); + ED_SHORTCUT("animation_editor/go_to_previous_keyframe", TTR("Go to Previous Keyframe"), KeyModifierMask::SHIFT + KeyModifierMask::ALT + Key::A); } AnimationPlayerEditor::~AnimationPlayerEditor() { @@ -2165,7 +2236,7 @@ void AnimationPlayerEditorPlugin::_property_keyed(const String &p_keyed, const V return; } te->_clear_selection(); - te->insert_value_key(p_keyed, p_value, p_advance); + te->insert_value_key(p_keyed, p_advance); } void AnimationPlayerEditorPlugin::_transform_key_request(Object *sp, const String &p_sub, const Transform3D &p_key) { diff --git a/editor/plugins/animation_player_editor_plugin.h b/editor/plugins/animation_player_editor_plugin.h index 4a3b1f37ab..860d421b91 100644 --- a/editor/plugins/animation_player_editor_plugin.h +++ b/editor/plugins/animation_player_editor_plugin.h @@ -178,6 +178,7 @@ class AnimationPlayerEditor : public VBoxContainer { void _select_anim_by_name(const String &p_anim); float _get_editor_step() const; + void _go_to_nearest_keyframe(bool p_backward); void _play_pressed(); void _play_from_pressed(); void _play_bw_pressed(); @@ -216,6 +217,7 @@ class AnimationPlayerEditor : public VBoxContainer { void _animation_key_editor_seek(float p_pos, bool p_timeline_only = false, bool p_update_position_only = false); void _animation_key_editor_anim_len_changed(float p_len); + void _animation_update_key_frame(); virtual void shortcut_input(const Ref<InputEvent> &p_ev) override; void _animation_tool_menu(int p_option); diff --git a/editor/plugins/bone_map_editor_plugin.cpp b/editor/plugins/bone_map_editor_plugin.cpp index 015dfdbca5..32ff478c33 100644 --- a/editor/plugins/bone_map_editor_plugin.cpp +++ b/editor/plugins/bone_map_editor_plugin.cpp @@ -1229,9 +1229,11 @@ void BoneMapper::auto_mapping_process(Ref<BoneMap> &p_bone_map) { picklist.push_back("face"); int head = search_bone_by_name(skeleton, picklist, BONE_SEGREGATION_NONE, neck); if (head == -1) { - search_path = skeleton->get_bone_children(neck); - if (search_path.size() == 1) { - head = search_path[0]; // Maybe only one child of the Neck is Head. + if (neck != -1) { + search_path = skeleton->get_bone_children(neck); + if (search_path.size() == 1) { + head = search_path[0]; // Maybe only one child of the Neck is Head. + } } } if (head == -1) { diff --git a/editor/plugins/canvas_item_editor_plugin.cpp b/editor/plugins/canvas_item_editor_plugin.cpp index 1afe2ddda7..6e41e98360 100644 --- a/editor/plugins/canvas_item_editor_plugin.cpp +++ b/editor/plugins/canvas_item_editor_plugin.cpp @@ -64,6 +64,7 @@ #include "scene/resources/style_box_texture.h" #define RULER_WIDTH (15 * EDSCALE) +#define DRAG_THRESHOLD (8 * EDSCALE) constexpr real_t SCALE_HANDLE_DISTANCE = 25; constexpr real_t MOVE_HANDLE_DISTANCE = 25; @@ -2319,7 +2320,7 @@ bool CanvasItemEditor::_gui_input_select(const Ref<InputEvent> &p_event) { Ref<InputEventMouseMotion> m = p_event; Ref<InputEventKey> k = p_event; - if (drag_type == DRAG_NONE) { + if (drag_type == DRAG_NONE || (drag_type == DRAG_BOX_SELECTION && b.is_valid() && !b->is_pressed())) { if (b.is_valid() && b->is_pressed() && ((b->get_button_index() == MouseButton::RIGHT && b->is_alt_pressed() && tool == TOOL_SELECT) || (b->get_button_index() == MouseButton::LEFT && tool == TOOL_LIST_SELECT))) { @@ -2411,47 +2412,58 @@ bool CanvasItemEditor::_gui_input_select(const Ref<InputEvent> &p_event) { return true; } - if (b.is_valid() && b->get_button_index() == MouseButton::LEFT && b->is_pressed() && !panner->is_panning() && (tool == TOOL_SELECT || tool == TOOL_MOVE || tool == TOOL_SCALE || tool == TOOL_ROTATE)) { - // Single item selection - Point2 click = transform.affine_inverse().xform(b->get_position()); + Point2 click; + bool can_select = b.is_valid() && b->get_button_index() == MouseButton::LEFT && !panner->is_panning() && (tool == TOOL_SELECT || tool == TOOL_MOVE || tool == TOOL_SCALE || tool == TOOL_ROTATE); + if (can_select) { + click = transform.affine_inverse().xform(b->get_position()); + // Allow selecting on release when performed very small box selection (necessary when Shift is pressed, see below). + can_select = b->is_pressed() || (drag_type == DRAG_BOX_SELECTION && click.distance_to(drag_from) <= DRAG_THRESHOLD); + } + if (can_select) { + // Single item selection. Node *scene = EditorNode::get_singleton()->get_edited_scene(); if (!scene) { return true; } - // Find the item to select + // Find the item to select. CanvasItem *ci = nullptr; Vector<_SelectResult> selection = Vector<_SelectResult>(); - // Retrieve the canvas items + // Retrieve the canvas items. _get_canvas_items_at_pos(click, selection); if (!selection.is_empty()) { ci = selection[0].item; } - if (!ci) { - // Start a box selection + // Shift also allows forcing box selection when item was clicked. + if (!ci || (b->is_shift_pressed() && b->is_pressed())) { + // Start a box selection. if (!b->is_shift_pressed()) { - // Clear the selection if not additive + // Clear the selection if not additive. editor_selection->clear(); viewport->queue_redraw(); selected_from_canvas = true; }; - drag_from = click; - drag_type = DRAG_BOX_SELECTION; - box_selecting_to = drag_from; - return true; + if (b->is_pressed()) { + drag_from = click; + drag_type = DRAG_BOX_SELECTION; + box_selecting_to = drag_from; + return true; + } } else { bool still_selected = _select_click_on_item(ci, click, b->is_shift_pressed()); - // Start dragging - if (still_selected && (tool == TOOL_SELECT || tool == TOOL_MOVE)) { - // Drag the node(s) if requested + // Start dragging. + if (still_selected && (tool == TOOL_SELECT || tool == TOOL_MOVE) && b->is_pressed()) { + // Drag the node(s) if requested. drag_start_origin = click; drag_type = DRAG_QUEUED; + } else if (!b->is_pressed()) { + _reset_drag(); } - // Select the item + // Select the item. return true; } } @@ -4319,13 +4331,13 @@ void CanvasItemEditor::_insert_animation_keys(bool p_location, bool p_rotation, Node2D *n2d = Object::cast_to<Node2D>(ci); if (key_pos && p_location) { - te->insert_node_value_key(n2d, "position", n2d->get_position(), p_on_existing); + te->insert_node_value_key(n2d, "position", p_on_existing); } if (key_rot && p_rotation) { - te->insert_node_value_key(n2d, "rotation", n2d->get_rotation(), p_on_existing); + te->insert_node_value_key(n2d, "rotation", p_on_existing); } if (key_scale && p_scale) { - te->insert_node_value_key(n2d, "scale", n2d->get_scale(), p_on_existing); + te->insert_node_value_key(n2d, "scale", p_on_existing); } if (n2d->has_meta("_edit_bone_") && n2d->get_parent_item()) { @@ -4351,13 +4363,13 @@ void CanvasItemEditor::_insert_animation_keys(bool p_location, bool p_rotation, if (has_chain && ik_chain.size()) { for (Node2D *&F : ik_chain) { if (key_pos) { - te->insert_node_value_key(F, "position", F->get_position(), p_on_existing); + te->insert_node_value_key(F, "position", p_on_existing); } if (key_rot) { - te->insert_node_value_key(F, "rotation", F->get_rotation(), p_on_existing); + te->insert_node_value_key(F, "rotation", p_on_existing); } if (key_scale) { - te->insert_node_value_key(F, "scale", F->get_scale(), p_on_existing); + te->insert_node_value_key(F, "scale", p_on_existing); } } } @@ -4367,13 +4379,13 @@ void CanvasItemEditor::_insert_animation_keys(bool p_location, bool p_rotation, Control *ctrl = Object::cast_to<Control>(ci); if (key_pos) { - te->insert_node_value_key(ctrl, "position", ctrl->get_position(), p_on_existing); + te->insert_node_value_key(ctrl, "position", p_on_existing); } if (key_rot) { - te->insert_node_value_key(ctrl, "rotation", ctrl->get_rotation(), p_on_existing); + te->insert_node_value_key(ctrl, "rotation", p_on_existing); } if (key_scale) { - te->insert_node_value_key(ctrl, "size", ctrl->get_size(), p_on_existing); + te->insert_node_value_key(ctrl, "size", p_on_existing); } } } @@ -5397,7 +5409,7 @@ CanvasItemEditor::CanvasItemEditor() { smart_snap_button->set_theme_type_variation("FlatButton"); main_menu_hbox->add_child(smart_snap_button); smart_snap_button->set_toggle_mode(true); - smart_snap_button->connect("toggled", callable_mp(this, &CanvasItemEditor::_button_toggle_smart_snap)); + smart_snap_button->connect(SceneStringName(toggled), callable_mp(this, &CanvasItemEditor::_button_toggle_smart_snap)); smart_snap_button->set_tooltip_text(TTR("Toggle smart snapping.")); smart_snap_button->set_shortcut(ED_SHORTCUT("canvas_item_editor/use_smart_snap", TTR("Use Smart Snap"), KeyModifierMask::SHIFT | Key::S)); smart_snap_button->set_shortcut_context(this); @@ -5406,7 +5418,7 @@ CanvasItemEditor::CanvasItemEditor() { grid_snap_button->set_theme_type_variation("FlatButton"); main_menu_hbox->add_child(grid_snap_button); grid_snap_button->set_toggle_mode(true); - grid_snap_button->connect("toggled", callable_mp(this, &CanvasItemEditor::_button_toggle_grid_snap)); + grid_snap_button->connect(SceneStringName(toggled), callable_mp(this, &CanvasItemEditor::_button_toggle_grid_snap)); grid_snap_button->set_tooltip_text(TTR("Toggle grid snapping.")); grid_snap_button->set_shortcut(ED_SHORTCUT("canvas_item_editor/use_grid_snap", TTR("Use Grid Snap"), KeyModifierMask::SHIFT | Key::G)); grid_snap_button->set_shortcut_context(this); @@ -5499,7 +5511,7 @@ CanvasItemEditor::CanvasItemEditor() { override_camera_button = memnew(Button); override_camera_button->set_theme_type_variation("FlatButton"); main_menu_hbox->add_child(override_camera_button); - override_camera_button->connect("toggled", callable_mp(this, &CanvasItemEditor::_button_override_camera)); + override_camera_button->connect(SceneStringName(toggled), callable_mp(this, &CanvasItemEditor::_button_override_camera)); override_camera_button->set_toggle_mode(true); override_camera_button->set_disabled(true); _update_override_camera_button(false); diff --git a/editor/plugins/control_editor_plugin.cpp b/editor/plugins/control_editor_plugin.cpp index df20395ac5..5c5f236ff3 100644 --- a/editor/plugins/control_editor_plugin.cpp +++ b/editor/plugins/control_editor_plugin.cpp @@ -1084,7 +1084,7 @@ ControlEditorToolbar::ControlEditorToolbar() { anchor_mode_button->set_toggle_mode(true); anchor_mode_button->set_tooltip_text(TTR("When active, moving Control nodes changes their anchors instead of their offsets.")); add_child(anchor_mode_button); - anchor_mode_button->connect("toggled", callable_mp(this, &ControlEditorToolbar::_anchor_mode_toggled)); + anchor_mode_button->connect(SceneStringName(toggled), callable_mp(this, &ControlEditorToolbar::_anchor_mode_toggled)); // Container tools. containers_button = memnew(ControlEditorPopupButton); diff --git a/editor/plugins/curve_editor_plugin.cpp b/editor/plugins/curve_editor_plugin.cpp index 180de700b7..e518cf7815 100644 --- a/editor/plugins/curve_editor_plugin.cpp +++ b/editor/plugins/curve_editor_plugin.cpp @@ -1003,7 +1003,7 @@ CurveEditor::CurveEditor() { snap_button->set_tooltip_text(TTR("Toggle Grid Snap")); snap_button->set_toggle_mode(true); toolbar->add_child(snap_button); - snap_button->connect("toggled", callable_mp(this, &CurveEditor::_set_snap_enabled)); + snap_button->connect(SceneStringName(toggled), callable_mp(this, &CurveEditor::_set_snap_enabled)); toolbar->add_child(memnew(VSeparator)); diff --git a/editor/plugins/editor_plugin.cpp b/editor/plugins/editor_plugin.cpp index d9f60e155d..8ce667568f 100644 --- a/editor/plugins/editor_plugin.cpp +++ b/editor/plugins/editor_plugin.cpp @@ -40,6 +40,7 @@ #include "editor/editor_translation_parser.h" #include "editor/editor_undo_redo_manager.h" #include "editor/export/editor_export.h" +#include "editor/export/editor_export_platform.h" #include "editor/gui/editor_bottom_panel.h" #include "editor/gui/editor_title_bar.h" #include "editor/import/3d/resource_importer_scene.h" @@ -441,6 +442,16 @@ void EditorPlugin::remove_export_plugin(const Ref<EditorExportPlugin> &p_exporte EditorExport::get_singleton()->remove_export_plugin(p_exporter); } +void EditorPlugin::add_export_platform(const Ref<EditorExportPlatform> &p_platform) { + ERR_FAIL_COND(p_platform.is_null()); + EditorExport::get_singleton()->add_export_platform(p_platform); +} + +void EditorPlugin::remove_export_platform(const Ref<EditorExportPlatform> &p_platform) { + ERR_FAIL_COND(p_platform.is_null()); + EditorExport::get_singleton()->remove_export_platform(p_platform); +} + void EditorPlugin::add_node_3d_gizmo_plugin(const Ref<EditorNode3DGizmoPlugin> &p_gizmo_plugin) { ERR_FAIL_COND(!p_gizmo_plugin.is_valid()); Node3DEditor::get_singleton()->add_gizmo_plugin(p_gizmo_plugin); @@ -608,6 +619,8 @@ void EditorPlugin::_bind_methods() { ClassDB::bind_method(D_METHOD("remove_scene_post_import_plugin", "scene_import_plugin"), &EditorPlugin::remove_scene_post_import_plugin); ClassDB::bind_method(D_METHOD("add_export_plugin", "plugin"), &EditorPlugin::add_export_plugin); ClassDB::bind_method(D_METHOD("remove_export_plugin", "plugin"), &EditorPlugin::remove_export_plugin); + ClassDB::bind_method(D_METHOD("add_export_platform", "platform"), &EditorPlugin::add_export_platform); + ClassDB::bind_method(D_METHOD("remove_export_platform", "platform"), &EditorPlugin::remove_export_platform); ClassDB::bind_method(D_METHOD("add_node_3d_gizmo_plugin", "plugin"), &EditorPlugin::add_node_3d_gizmo_plugin); ClassDB::bind_method(D_METHOD("remove_node_3d_gizmo_plugin", "plugin"), &EditorPlugin::remove_node_3d_gizmo_plugin); ClassDB::bind_method(D_METHOD("add_inspector_plugin", "plugin"), &EditorPlugin::add_inspector_plugin); diff --git a/editor/plugins/editor_plugin.h b/editor/plugins/editor_plugin.h index f6c4b35407..2e0771f906 100644 --- a/editor/plugins/editor_plugin.h +++ b/editor/plugins/editor_plugin.h @@ -41,6 +41,7 @@ class PopupMenu; class EditorDebuggerPlugin; class EditorExport; class EditorExportPlugin; +class EditorExportPlatform; class EditorImportPlugin; class EditorInspectorPlugin; class EditorInterface; @@ -224,6 +225,9 @@ public: void add_export_plugin(const Ref<EditorExportPlugin> &p_exporter); void remove_export_plugin(const Ref<EditorExportPlugin> &p_exporter); + void add_export_platform(const Ref<EditorExportPlatform> &p_platform); + void remove_export_platform(const Ref<EditorExportPlatform> &p_platform); + void add_node_3d_gizmo_plugin(const Ref<EditorNode3DGizmoPlugin> &p_gizmo_plugin); void remove_node_3d_gizmo_plugin(const Ref<EditorNode3DGizmoPlugin> &p_gizmo_plugin); diff --git a/editor/plugins/font_config_plugin.cpp b/editor/plugins/font_config_plugin.cpp index d712c14861..ec9513363d 100644 --- a/editor/plugins/font_config_plugin.cpp +++ b/editor/plugins/font_config_plugin.cpp @@ -922,7 +922,8 @@ void FontPreview::_notification(int p_what) { name = vformat("%s (%s)", prev_font->get_font_name(), prev_font->get_font_style_name()); } if (prev_font->is_class("FontVariation")) { - name += " " + TTR(" - Variation"); + // TRANSLATORS: This refers to variable font config, appended to the font name. + name += " - " + TTR("Variation"); } font->draw_string(get_canvas_item(), Point2(0, font->get_height(font_size) + 2 * EDSCALE), name, HORIZONTAL_ALIGNMENT_CENTER, get_size().x, font_size, text_color); diff --git a/editor/plugins/gdextension_export_plugin.h b/editor/plugins/gdextension_export_plugin.h index da136b70ae..0de6b7b611 100644 --- a/editor/plugins/gdextension_export_plugin.h +++ b/editor/plugins/gdextension_export_plugin.h @@ -31,6 +31,7 @@ #ifndef GDEXTENSION_EXPORT_PLUGIN_H #define GDEXTENSION_EXPORT_PLUGIN_H +#include "core/extension/gdextension_library_loader.h" #include "editor/export/editor_export.h" class GDExtensionExportPlugin : public EditorExportPlugin { @@ -92,7 +93,7 @@ void GDExtensionExportPlugin::_export_file(const String &p_path, const String &p for (const String &arch_tag : archs) { PackedStringArray tags; - String library_path = GDExtension::find_extension_library( + String library_path = GDExtensionLibraryLoader::find_extension_library( p_path, config, [features_wo_arch, arch_tag](const String &p_feature) { return features_wo_arch.has(p_feature) || (p_feature == arch_tag); }, &tags); if (libs_added.has(library_path)) { continue; // Universal library, already added for another arch, do not duplicate. @@ -129,7 +130,7 @@ void GDExtensionExportPlugin::_export_file(const String &p_path, const String &p ERR_FAIL_MSG(vformat("No suitable library found for GDExtension: %s. Possible feature flags for your platform: %s", p_path, String(", ").join(features_vector))); } - Vector<SharedObject> dependencies_shared_objects = GDExtension::find_extension_dependencies(p_path, config, [p_features](String p_feature) { return p_features.has(p_feature); }); + Vector<SharedObject> dependencies_shared_objects = GDExtensionLibraryLoader::find_extension_dependencies(p_path, config, [p_features](String p_feature) { return p_features.has(p_feature); }); for (const SharedObject &shared_object : dependencies_shared_objects) { _add_shared_object(shared_object); } diff --git a/editor/plugins/gizmos/joint_3d_gizmo_plugin.cpp b/editor/plugins/gizmos/joint_3d_gizmo_plugin.cpp index ae24b4250e..c277ec8cd3 100644 --- a/editor/plugins/gizmos/joint_3d_gizmo_plugin.cpp +++ b/editor/plugins/gizmos/joint_3d_gizmo_plugin.cpp @@ -293,9 +293,15 @@ Joint3DGizmoPlugin::Joint3DGizmoPlugin() { void Joint3DGizmoPlugin::incremental_update_gizmos() { if (!current_gizmos.is_empty()) { - update_idx++; - update_idx = update_idx % current_gizmos.size(); - redraw(current_gizmos.get(update_idx)); + HashSet<EditorNode3DGizmo *>::Iterator E = current_gizmos.find(last_drawn); + if (E) { + ++E; + } + if (!E) { + E = current_gizmos.begin(); + } + redraw(*E); + last_drawn = *E; } } diff --git a/editor/plugins/gizmos/joint_3d_gizmo_plugin.h b/editor/plugins/gizmos/joint_3d_gizmo_plugin.h index 79fe40d1b2..25b08d71c6 100644 --- a/editor/plugins/gizmos/joint_3d_gizmo_plugin.h +++ b/editor/plugins/gizmos/joint_3d_gizmo_plugin.h @@ -37,7 +37,7 @@ class Joint3DGizmoPlugin : public EditorNode3DGizmoPlugin { GDCLASS(Joint3DGizmoPlugin, EditorNode3DGizmoPlugin); Timer *update_timer = nullptr; - uint64_t update_idx = 0; + EditorNode3DGizmo *last_drawn = nullptr; void incremental_update_gizmos(); diff --git a/editor/plugins/gradient_editor_plugin.cpp b/editor/plugins/gradient_editor_plugin.cpp index 8bf5dad97f..1300394ca3 100644 --- a/editor/plugins/gradient_editor_plugin.cpp +++ b/editor/plugins/gradient_editor_plugin.cpp @@ -632,7 +632,7 @@ GradientEditor::GradientEditor() { snap_button->set_tooltip_text(TTR("Toggle Grid Snap")); snap_button->set_toggle_mode(true); toolbar->add_child(snap_button); - snap_button->connect("toggled", callable_mp(this, &GradientEditor::_set_snap_enabled)); + snap_button->connect(SceneStringName(toggled), callable_mp(this, &GradientEditor::_set_snap_enabled)); snap_count_edit = memnew(EditorSpinSlider); snap_count_edit->set_min(2); diff --git a/editor/plugins/gradient_texture_2d_editor_plugin.cpp b/editor/plugins/gradient_texture_2d_editor_plugin.cpp index 7e22e1209c..5bf1422780 100644 --- a/editor/plugins/gradient_texture_2d_editor_plugin.cpp +++ b/editor/plugins/gradient_texture_2d_editor_plugin.cpp @@ -290,7 +290,7 @@ GradientTexture2DEditor::GradientTexture2DEditor() { snap_button->set_tooltip_text(TTR("Toggle Grid Snap")); snap_button->set_toggle_mode(true); toolbar->add_child(snap_button); - snap_button->connect("toggled", callable_mp(this, &GradientTexture2DEditor::_set_snap_enabled)); + snap_button->connect(SceneStringName(toggled), callable_mp(this, &GradientTexture2DEditor::_set_snap_enabled)); snap_count_edit = memnew(EditorSpinSlider); snap_count_edit->set_min(2); diff --git a/editor/plugins/material_editor_plugin.cpp b/editor/plugins/material_editor_plugin.cpp index 602e6f945c..2702b6c909 100644 --- a/editor/plugins/material_editor_plugin.cpp +++ b/editor/plugins/material_editor_plugin.cpp @@ -33,6 +33,7 @@ #include "core/config/project_settings.h" #include "editor/editor_node.h" #include "editor/editor_settings.h" +#include "editor/editor_string_names.h" #include "editor/editor_undo_redo_manager.h" #include "editor/themes/editor_scale.h" #include "scene/3d/camera_3d.h" @@ -41,6 +42,7 @@ #include "scene/gui/box_container.h" #include "scene/gui/button.h" #include "scene/gui/color_rect.h" +#include "scene/gui/label.h" #include "scene/gui/subviewport_container.h" #include "scene/main/viewport.h" #include "scene/resources/3d/fog_material.h" @@ -80,11 +82,15 @@ void MaterialEditor::_notification(int p_what) { sphere_switch->set_icon(theme_cache.sphere_icon); box_switch->set_icon(theme_cache.box_icon); + + error_label->add_theme_color_override(SceneStringName(font_color), get_theme_color(SNAME("error_color"), EditorStringName(Editor))); } break; case NOTIFICATION_DRAW: { - Size2 size = get_size(); - draw_texture_rect(theme_cache.checkerboard, Rect2(Point2(), size), true); + if (!is_unsupported_shader_mode) { + Size2 size = get_size(); + draw_texture_rect(theme_cache.checkerboard, Rect2(Point2(), size), true); + } } break; } } @@ -99,16 +105,20 @@ void MaterialEditor::_update_rotation() { void MaterialEditor::edit(Ref<Material> p_material, const Ref<Environment> &p_env) { material = p_material; camera->set_environment(p_env); + + is_unsupported_shader_mode = false; if (!material.is_null()) { Shader::Mode mode = p_material->get_shader_mode(); switch (mode) { case Shader::MODE_CANVAS_ITEM: + layout_error->hide(); layout_3d->hide(); layout_2d->show(); vc->hide(); rect_instance->set_material(material); break; case Shader::MODE_SPATIAL: + layout_error->hide(); layout_2d->hide(); layout_3d->show(); vc->show(); @@ -116,6 +126,11 @@ void MaterialEditor::edit(Ref<Material> p_material, const Ref<Environment> &p_en box_instance->set_material_override(material); break; default: + layout_error->show(); + layout_2d->hide(); + layout_3d->hide(); + vc->hide(); + is_unsupported_shader_mode = true; break; } } else { @@ -175,6 +190,20 @@ MaterialEditor::MaterialEditor() { layout_2d->set_visible(false); + layout_error = memnew(VBoxContainer); + layout_error->set_alignment(BoxContainer::ALIGNMENT_CENTER); + layout_error->set_anchors_and_offsets_preset(PRESET_FULL_RECT); + + error_label = memnew(Label); + error_label->set_text(TTR("Preview is not available for this shader mode.")); + error_label->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER); + error_label->set_vertical_alignment(VERTICAL_ALIGNMENT_CENTER); + error_label->set_autowrap_mode(TextServer::AUTOWRAP_WORD_SMART); + + layout_error->add_child(error_label); + layout_error->hide(); + add_child(layout_error); + // Spatial vc = memnew(SubViewportContainer); diff --git a/editor/plugins/material_editor_plugin.h b/editor/plugins/material_editor_plugin.h index fb6bafc0ef..28c59d27db 100644 --- a/editor/plugins/material_editor_plugin.h +++ b/editor/plugins/material_editor_plugin.h @@ -45,6 +45,7 @@ class MeshInstance3D; class SubViewport; class SubViewportContainer; class Button; +class Label; class MaterialEditor : public Control { GDCLASS(MaterialEditor, Control); @@ -69,6 +70,10 @@ class MaterialEditor : public Control { Ref<SphereMesh> sphere_mesh; Ref<BoxMesh> box_mesh; + VBoxContainer *layout_error = nullptr; + Label *error_label = nullptr; + bool is_unsupported_shader_mode = false; + HBoxContainer *layout_3d = nullptr; Ref<Material> material; diff --git a/editor/plugins/node_3d_editor_gizmos.cpp b/editor/plugins/node_3d_editor_gizmos.cpp index 67d5e44ce5..1b2cd441ad 100644 --- a/editor/plugins/node_3d_editor_gizmos.cpp +++ b/editor/plugins/node_3d_editor_gizmos.cpp @@ -1060,7 +1060,7 @@ Ref<EditorNode3DGizmo> EditorNode3DGizmoPlugin::get_gizmo(Node3D *p_spatial) { ref->set_node_3d(p_spatial); ref->set_hidden(current_state == HIDDEN); - current_gizmos.push_back(ref.ptr()); + current_gizmos.insert(ref.ptr()); return ref; } diff --git a/editor/plugins/node_3d_editor_gizmos.h b/editor/plugins/node_3d_editor_gizmos.h index c4b275032a..1916bc2058 100644 --- a/editor/plugins/node_3d_editor_gizmos.h +++ b/editor/plugins/node_3d_editor_gizmos.h @@ -157,7 +157,7 @@ public: protected: int current_state; - List<EditorNode3DGizmo *> current_gizmos; + HashSet<EditorNode3DGizmo *> current_gizmos; HashMap<String, Vector<Ref<StandardMaterial3D>>> materials; static void _bind_methods(); diff --git a/editor/plugins/node_3d_editor_plugin.cpp b/editor/plugins/node_3d_editor_plugin.cpp index dc87bec584..8b0f4a64a7 100644 --- a/editor/plugins/node_3d_editor_plugin.cpp +++ b/editor/plugins/node_3d_editor_plugin.cpp @@ -770,7 +770,7 @@ void Node3DEditorViewport::_select_clicked(bool p_allow_locked) { } } - if (p_allow_locked || !_is_node_locked(selected)) { + if (p_allow_locked || (selected != nullptr && !_is_node_locked(selected))) { if (clicked_wants_append) { if (editor_selection->is_selected(selected)) { editor_selection->remove_node(selected); @@ -1178,7 +1178,7 @@ void Node3DEditorViewport::_update_name() { if (auto_orthogonal) { // TRANSLATORS: This will be appended to the view name when Auto Orthogonal is enabled. - name += TTR(" [auto]"); + name += " " + TTR("[auto]"); } view_menu->set_text(name); @@ -1692,6 +1692,10 @@ void Node3DEditorViewport::_sinput(const Ref<InputEvent> &p_event) { if (b.is_valid()) { emit_signal(SNAME("clicked"), this); + ViewportNavMouseButton orbit_mouse_preference = (ViewportNavMouseButton)EDITOR_GET("editors/3d/navigation/orbit_mouse_button").operator int(); + ViewportNavMouseButton pan_mouse_preference = (ViewportNavMouseButton)EDITOR_GET("editors/3d/navigation/pan_mouse_button").operator int(); + ViewportNavMouseButton zoom_mouse_preference = (ViewportNavMouseButton)EDITOR_GET("editors/3d/navigation/zoom_mouse_button").operator int(); + const real_t zoom_factor = 1 + (ZOOM_FREELOOK_MULTIPLIER - 1) * b->get_factor(); switch (b->get_button_index()) { case MouseButton::WHEEL_UP: { @@ -1709,8 +1713,6 @@ void Node3DEditorViewport::_sinput(const Ref<InputEvent> &p_event) { } } break; case MouseButton::RIGHT: { - NavigationScheme nav_scheme = (NavigationScheme)EDITOR_GET("editors/3d/navigation/navigation_scheme").operator int(); - if (b->is_pressed() && _edit.gizmo.is_valid()) { //restore _edit.gizmo->commit_handle(_edit.gizmo_handle, _edit.gizmo_handle_secondary, _edit.gizmo_initial_value, true); @@ -1718,11 +1720,15 @@ void Node3DEditorViewport::_sinput(const Ref<InputEvent> &p_event) { } if (_edit.mode == TRANSFORM_NONE && b->is_pressed()) { - if (b->is_alt_pressed()) { - if (nav_scheme == NAVIGATION_MAYA) { - break; - } + if (orbit_mouse_preference == NAVIGATION_RIGHT_MOUSE && _is_nav_modifier_pressed("spatial_editor/viewport_orbit_modifier_1") && _is_nav_modifier_pressed("spatial_editor/viewport_orbit_modifier_2")) { + break; + } else if (pan_mouse_preference == NAVIGATION_RIGHT_MOUSE && _is_nav_modifier_pressed("spatial_editor/viewport_pan_modifier_1") && _is_nav_modifier_pressed("spatial_editor/viewport_pan_modifier_2")) { + break; + } else if (zoom_mouse_preference == NAVIGATION_RIGHT_MOUSE && _is_nav_modifier_pressed("spatial_editor/viewport_zoom_modifier_1") && _is_nav_modifier_pressed("spatial_editor/viewport_zoom_modifier_2")) { + break; + } + if (b->is_alt_pressed()) { _list_select(b); return; } @@ -1753,6 +1759,14 @@ void Node3DEditorViewport::_sinput(const Ref<InputEvent> &p_event) { } break; case MouseButton::MIDDLE: { if (b->is_pressed() && _edit.mode != TRANSFORM_NONE) { + if (orbit_mouse_preference == NAVIGATION_MIDDLE_MOUSE && _is_nav_modifier_pressed("spatial_editor/viewport_orbit_modifier_1") && _is_nav_modifier_pressed("spatial_editor/viewport_orbit_modifier_2")) { + break; + } else if (pan_mouse_preference == NAVIGATION_MIDDLE_MOUSE && _is_nav_modifier_pressed("spatial_editor/viewport_pan_modifier_1") && _is_nav_modifier_pressed("spatial_editor/viewport_pan_modifier_2")) { + break; + } else if (zoom_mouse_preference == NAVIGATION_MIDDLE_MOUSE && _is_nav_modifier_pressed("spatial_editor/viewport_zoom_modifier_1") && _is_nav_modifier_pressed("spatial_editor/viewport_zoom_modifier_2")) { + break; + } + switch (_edit.plane) { case TRANSFORM_VIEW: { _edit.plane = TRANSFORM_X_AXIS; @@ -1791,8 +1805,11 @@ void Node3DEditorViewport::_sinput(const Ref<InputEvent> &p_event) { commit_transform(); break; // just commit the edit, stop processing the event so we don't deselect the object } - NavigationScheme nav_scheme = (NavigationScheme)EDITOR_GET("editors/3d/navigation/navigation_scheme").operator int(); - if ((nav_scheme == NAVIGATION_MAYA || nav_scheme == NAVIGATION_MODO) && b->is_alt_pressed()) { + if (orbit_mouse_preference == NAVIGATION_LEFT_MOUSE && _is_nav_modifier_pressed("spatial_editor/viewport_orbit_modifier_1") && _is_nav_modifier_pressed("spatial_editor/viewport_orbit_modifier_2")) { + break; + } else if (pan_mouse_preference == NAVIGATION_LEFT_MOUSE && _is_nav_modifier_pressed("spatial_editor/viewport_pan_modifier_1") && _is_nav_modifier_pressed("spatial_editor/viewport_pan_modifier_2")) { + break; + } else if (zoom_mouse_preference == NAVIGATION_LEFT_MOUSE && _is_nav_modifier_pressed("spatial_editor/viewport_zoom_modifier_1") && _is_nav_modifier_pressed("spatial_editor/viewport_zoom_modifier_2")) { break; } @@ -1811,7 +1828,9 @@ void Node3DEditorViewport::_sinput(const Ref<InputEvent> &p_event) { { int idx = view_menu->get_popup()->get_item_index(VIEW_GIZMOS); + int idx2 = view_menu->get_popup()->get_item_index(VIEW_TRANSFORM_GIZMO); can_select_gizmos = can_select_gizmos && view_menu->get_popup()->is_item_checked(idx); + transform_gizmo_visible = view_menu->get_popup()->is_item_checked(idx2); } // Gizmo handles @@ -1988,6 +2007,24 @@ void Node3DEditorViewport::_sinput(const Ref<InputEvent> &p_event) { } } + ViewportNavMouseButton orbit_mouse_preference = (ViewportNavMouseButton)EDITOR_GET("editors/3d/navigation/orbit_mouse_button").operator int(); + ViewportNavMouseButton pan_mouse_preference = (ViewportNavMouseButton)EDITOR_GET("editors/3d/navigation/pan_mouse_button").operator int(); + ViewportNavMouseButton zoom_mouse_preference = (ViewportNavMouseButton)EDITOR_GET("editors/3d/navigation/zoom_mouse_button").operator int(); + bool orbit_mod_pressed = _is_nav_modifier_pressed("spatial_editor/viewport_orbit_modifier_1") && _is_nav_modifier_pressed("spatial_editor/viewport_orbit_modifier_2"); + bool pan_mod_pressed = _is_nav_modifier_pressed("spatial_editor/viewport_pan_modifier_1") && _is_nav_modifier_pressed("spatial_editor/viewport_pan_modifier_2"); + bool zoom_mod_pressed = _is_nav_modifier_pressed("spatial_editor/viewport_zoom_modifier_1") && _is_nav_modifier_pressed("spatial_editor/viewport_zoom_modifier_2"); + int orbit_mod_input_count = _get_shortcut_input_count("spatial_editor/viewport_orbit_modifier_1") + _get_shortcut_input_count("spatial_editor/viewport_orbit_modifier_2"); + int pan_mod_input_count = _get_shortcut_input_count("spatial_editor/viewport_pan_modifier_1") + _get_shortcut_input_count("spatial_editor/viewport_pan_modifier_2"); + int zoom_mod_input_count = _get_shortcut_input_count("spatial_editor/viewport_zoom_modifier_1") + _get_shortcut_input_count("spatial_editor/viewport_zoom_modifier_2"); + bool orbit_not_empty = !_is_shortcut_empty("spatial_editor/viewport_zoom_modifier_1") || !_is_shortcut_empty("spatial_editor/viewport_zoom_modifier_2"); + bool pan_not_empty = !_is_shortcut_empty("spatial_editor/viewport_pan_modifier_1") || !_is_shortcut_empty("spatial_editor/viewport_pan_modifier_2"); + bool zoom_not_empty = !_is_shortcut_empty("spatial_editor/viewport_orbit_modifier_1") || !_is_shortcut_empty("spatial_editor/viewport_orbit_modifier_2"); + Vector<ShortcutCheckSet> shortcut_check_sets; + shortcut_check_sets.push_back(ShortcutCheckSet(orbit_mod_pressed, orbit_not_empty, orbit_mod_input_count, orbit_mouse_preference, NAVIGATION_ORBIT)); + shortcut_check_sets.push_back(ShortcutCheckSet(pan_mod_pressed, pan_not_empty, pan_mod_input_count, pan_mouse_preference, NAVIGATION_PAN)); + shortcut_check_sets.push_back(ShortcutCheckSet(zoom_mod_pressed, zoom_not_empty, zoom_mod_input_count, zoom_mouse_preference, NAVIGATION_ZOOM)); + shortcut_check_sets.sort_custom<ShortcutCheckSetComparator>(); + Ref<InputEventMouseMotion> m = p_event; // Instant transforms process mouse motion in input() to handle wrapping. @@ -2032,7 +2069,6 @@ void Node3DEditorViewport::_sinput(const Ref<InputEvent> &p_event) { _transform_gizmo_select(_edit.mouse_pos, true); } - NavigationScheme nav_scheme = (NavigationScheme)EDITOR_GET("editors/3d/navigation/navigation_scheme").operator int(); NavigationMode nav_mode = NAVIGATION_NONE; if (_edit.gizmo.is_valid()) { @@ -2042,14 +2078,9 @@ void Node3DEditorViewport::_sinput(const Ref<InputEvent> &p_event) { set_message(n + ": " + String(v)); } else if (m->get_button_mask().has_flag(MouseButtonMask::LEFT)) { - if (nav_scheme == NAVIGATION_MAYA && m->is_alt_pressed()) { - nav_mode = NAVIGATION_ORBIT; - } else if (nav_scheme == NAVIGATION_MODO && m->is_alt_pressed() && m->is_shift_pressed()) { - nav_mode = NAVIGATION_PAN; - } else if (nav_scheme == NAVIGATION_MODO && m->is_alt_pressed() && m->is_command_or_control_pressed()) { - nav_mode = NAVIGATION_ZOOM; - } else if (nav_scheme == NAVIGATION_MODO && m->is_alt_pressed()) { - nav_mode = NAVIGATION_ORBIT; + NavigationMode change_nav_from_shortcut = _get_nav_mode_from_shortcut_check(NAVIGATION_LEFT_MOUSE, shortcut_check_sets, false); + if (change_nav_from_shortcut != NAVIGATION_NONE) { + nav_mode = change_nav_from_shortcut; } else { const bool movement_threshold_passed = _edit.original_mouse_pos.distance_to(_edit.mouse_pos) > 8 * EDSCALE; @@ -2080,8 +2111,9 @@ void Node3DEditorViewport::_sinput(const Ref<InputEvent> &p_event) { update_transform(_get_key_modifier(m) == Key::SHIFT); } } else if (m->get_button_mask().has_flag(MouseButtonMask::RIGHT) || freelook_active) { - if (nav_scheme == NAVIGATION_MAYA && m->is_alt_pressed()) { - nav_mode = NAVIGATION_ZOOM; + NavigationMode change_nav_from_shortcut = _get_nav_mode_from_shortcut_check(NAVIGATION_RIGHT_MOUSE, shortcut_check_sets, false); + if (m->get_button_mask().has_flag(MouseButtonMask::RIGHT) && change_nav_from_shortcut != NAVIGATION_NONE) { + nav_mode = change_nav_from_shortcut; } else if (freelook_active) { nav_mode = NAVIGATION_LOOK; } else if (orthogonal) { @@ -2089,34 +2121,16 @@ void Node3DEditorViewport::_sinput(const Ref<InputEvent> &p_event) { } } else if (m->get_button_mask().has_flag(MouseButtonMask::MIDDLE)) { - const Key mod = _get_key_modifier(m); - if (nav_scheme == NAVIGATION_GODOT) { - if (mod == _get_key_modifier_setting("editors/3d/navigation/pan_modifier")) { - nav_mode = NAVIGATION_PAN; - } else if (mod == _get_key_modifier_setting("editors/3d/navigation/zoom_modifier")) { - nav_mode = NAVIGATION_ZOOM; - } else if (mod == Key::ALT || mod == _get_key_modifier_setting("editors/3d/navigation/orbit_modifier")) { - // Always allow Alt as a modifier to better support graphic tablets. - nav_mode = NAVIGATION_ORBIT; - } - } else if (nav_scheme == NAVIGATION_MAYA) { - if (mod == _get_key_modifier_setting("editors/3d/navigation/pan_modifier")) { - nav_mode = NAVIGATION_PAN; - } + NavigationMode change_nav_from_shortcut = _get_nav_mode_from_shortcut_check(NAVIGATION_MIDDLE_MOUSE, shortcut_check_sets, false); + if (change_nav_from_shortcut != NAVIGATION_NONE) { + nav_mode = change_nav_from_shortcut; } + } else if (EDITOR_GET("editors/3d/navigation/emulate_3_button_mouse")) { // Handle trackpad (no external mouse) use case - const Key mod = _get_key_modifier(m); - - if (mod != Key::NONE) { - if (mod == _get_key_modifier_setting("editors/3d/navigation/pan_modifier")) { - nav_mode = NAVIGATION_PAN; - } else if (mod == _get_key_modifier_setting("editors/3d/navigation/zoom_modifier")) { - nav_mode = NAVIGATION_ZOOM; - } else if (mod == Key::ALT || mod == _get_key_modifier_setting("editors/3d/navigation/orbit_modifier")) { - // Always allow Alt as a modifier to better support graphic tablets. - nav_mode = NAVIGATION_ORBIT; - } + NavigationMode change_nav_from_shortcut = _get_nav_mode_from_shortcut_check(NAVIGATION_LEFT_MOUSE, shortcut_check_sets, true); + if (change_nav_from_shortcut != NAVIGATION_NONE) { + nav_mode = change_nav_from_shortcut; } } @@ -2157,25 +2171,11 @@ void Node3DEditorViewport::_sinput(const Ref<InputEvent> &p_event) { Ref<InputEventPanGesture> pan_gesture = p_event; if (pan_gesture.is_valid()) { - NavigationScheme nav_scheme = (NavigationScheme)EDITOR_GET("editors/3d/navigation/navigation_scheme").operator int(); NavigationMode nav_mode = NAVIGATION_NONE; - if (nav_scheme == NAVIGATION_GODOT) { - const Key mod = _get_key_modifier(pan_gesture); - - if (mod == _get_key_modifier_setting("editors/3d/navigation/pan_modifier")) { - nav_mode = NAVIGATION_PAN; - } else if (mod == _get_key_modifier_setting("editors/3d/navigation/zoom_modifier")) { - nav_mode = NAVIGATION_ZOOM; - } else if (mod == Key::ALT || mod == _get_key_modifier_setting("editors/3d/navigation/orbit_modifier")) { - // Always allow Alt as a modifier to better support graphic tablets. - nav_mode = NAVIGATION_ORBIT; - } - - } else if (nav_scheme == NAVIGATION_MAYA) { - if (pan_gesture->is_alt_pressed()) { - nav_mode = NAVIGATION_PAN; - } + NavigationMode change_nav_from_shortcut = _get_nav_mode_from_shortcut_check(NAVIGATION_LEFT_MOUSE, shortcut_check_sets, true); + if (change_nav_from_shortcut != NAVIGATION_NONE) { + nav_mode = change_nav_from_shortcut; } switch (nav_mode) { @@ -2445,6 +2445,32 @@ void Node3DEditorViewport::_sinput(const Ref<InputEvent> &p_event) { } } +int Node3DEditorViewport::_get_shortcut_input_count(const String &p_name) { + Ref<Shortcut> check_shortcut = ED_GET_SHORTCUT(p_name); + + ERR_FAIL_COND_V_MSG(check_shortcut.is_null(), 0, "The Shortcut was null, possible name mismatch."); + + return check_shortcut->get_events().size(); +} + +Node3DEditorViewport::NavigationMode Node3DEditorViewport::_get_nav_mode_from_shortcut_check(ViewportNavMouseButton p_mouse_button, Vector<ShortcutCheckSet> p_shortcut_check_sets, bool p_use_not_empty) { + if (p_use_not_empty) { + for (const ShortcutCheckSet &shortcut_check_set : p_shortcut_check_sets) { + if (shortcut_check_set.mod_pressed && shortcut_check_set.shortcut_not_empty) { + return shortcut_check_set.result_nav_mode; + } + } + } else { + for (const ShortcutCheckSet &shortcut_check_set : p_shortcut_check_sets) { + if (shortcut_check_set.mouse_preference == p_mouse_button && shortcut_check_set.mod_pressed) { + return shortcut_check_set.result_nav_mode; + } + } + } + + return NAVIGATION_NONE; +} + void Node3DEditorViewport::_nav_pan(Ref<InputEventWithModifiers> p_event, const Vector2 &p_relative) { const NavigationScheme nav_scheme = (NavigationScheme)EDITOR_GET("editors/3d/navigation/navigation_scheme").operator int(); @@ -2644,6 +2670,18 @@ void Node3DEditorViewport::scale_freelook_speed(real_t scale) { surface->queue_redraw(); } +bool Node3DEditorViewport::_is_nav_modifier_pressed(const String &p_name) { + return _is_shortcut_empty(p_name) || Input::get_singleton()->is_action_pressed(p_name); +} + +bool Node3DEditorViewport::_is_shortcut_empty(const String &p_name) { + Ref<Shortcut> check_shortcut = ED_GET_SHORTCUT(p_name); + + ERR_FAIL_COND_V_MSG(check_shortcut.is_null(), true, "The Shortcut was null, possible name mismatch."); + + return check_shortcut->get_events().is_empty(); +} + Point2 Node3DEditorViewport::_get_warped_mouse_motion(const Ref<InputEventMouseMotion> &p_ev_mouse_motion) const { Point2 relative; if (bool(EDITOR_GET("editors/3d/navigation/warped_mouse_panning"))) { @@ -3539,6 +3577,15 @@ void Node3DEditorViewport::_menu_option(int p_option) { view_menu->get_popup()->set_item_checked(idx, current); } break; + case VIEW_TRANSFORM_GIZMO: { + int idx = view_menu->get_popup()->get_item_index(VIEW_TRANSFORM_GIZMO); + bool current = view_menu->get_popup()->is_item_checked(idx); + current = !current; + transform_gizmo_visible = current; + + spatial_editor->update_transform_gizmo(); + view_menu->get_popup()->set_item_checked(idx, current); + } break; case VIEW_HALF_RESOLUTION: { int idx = view_menu->get_popup()->get_item_index(VIEW_HALF_RESOLUTION); bool current = view_menu->get_popup()->is_item_checked(idx); @@ -3682,10 +3729,10 @@ void Node3DEditorViewport::_set_auto_orthogonal() { } void Node3DEditorViewport::_preview_exited_scene() { - preview_camera->disconnect("toggled", callable_mp(this, &Node3DEditorViewport::_toggle_camera_preview)); + preview_camera->disconnect(SceneStringName(toggled), callable_mp(this, &Node3DEditorViewport::_toggle_camera_preview)); preview_camera->set_pressed(false); _toggle_camera_preview(false); - preview_camera->connect("toggled", callable_mp(this, &Node3DEditorViewport::_toggle_camera_preview)); + preview_camera->connect(SceneStringName(toggled), callable_mp(this, &Node3DEditorViewport::_toggle_camera_preview)); view_menu->show(); } @@ -3903,7 +3950,7 @@ void Node3DEditorViewport::update_transform_gizmo_view() { return; } - bool show_gizmo = spatial_editor->is_gizmo_visible() && !_edit.instant; + bool show_gizmo = spatial_editor->is_gizmo_visible() && !_edit.instant && transform_gizmo_visible; for (int i = 0; i < 3; i++) { Transform3D axis_angle; if (xform.basis.get_column(i).normalized().dot(xform.basis.get_column((i + 1) % 3).normalized()) < 1.0) { @@ -3934,7 +3981,7 @@ void Node3DEditorViewport::update_transform_gizmo_view() { xform.orthonormalize(); xform.basis.scale(scale); RenderingServer::get_singleton()->instance_set_transform(rotate_gizmo_instance[3], xform); - RenderingServer::get_singleton()->instance_set_visible(rotate_gizmo_instance[3], spatial_editor->is_gizmo_visible() && (spatial_editor->get_tool_mode() == Node3DEditor::TOOL_MODE_SELECT || spatial_editor->get_tool_mode() == Node3DEditor::TOOL_MODE_ROTATE)); + RenderingServer::get_singleton()->instance_set_visible(rotate_gizmo_instance[3], spatial_editor->is_gizmo_visible() && transform_gizmo_visible && (spatial_editor->get_tool_mode() == Node3DEditor::TOOL_MODE_SELECT || spatial_editor->get_tool_mode() == Node3DEditor::TOOL_MODE_ROTATE)); } void Node3DEditorViewport::set_state(const Dictionary &p_state) { @@ -4011,6 +4058,14 @@ void Node3DEditorViewport::set_state(const Dictionary &p_state) { _menu_option(VIEW_GIZMOS); } } + if (p_state.has("transform_gizmo")) { + bool transform_gizmo = p_state["transform_gizmo"]; + + int idx = view_menu->get_popup()->get_item_index(VIEW_TRANSFORM_GIZMO); + if (view_menu->get_popup()->is_item_checked(idx) != transform_gizmo) { + _menu_option(VIEW_TRANSFORM_GIZMO); + } + } if (p_state.has("grid")) { bool grid = p_state["grid"]; @@ -4049,8 +4104,8 @@ void Node3DEditorViewport::set_state(const Dictionary &p_state) { view_menu->get_popup()->set_item_checked(idx, previewing_cinema); } - if (preview_camera->is_connected("toggled", callable_mp(this, &Node3DEditorViewport::_toggle_camera_preview))) { - preview_camera->disconnect("toggled", callable_mp(this, &Node3DEditorViewport::_toggle_camera_preview)); + if (preview_camera->is_connected(SceneStringName(toggled), callable_mp(this, &Node3DEditorViewport::_toggle_camera_preview))) { + preview_camera->disconnect(SceneStringName(toggled), callable_mp(this, &Node3DEditorViewport::_toggle_camera_preview)); } if (p_state.has("previewing")) { Node *pv = EditorNode::get_singleton()->get_edited_scene()->get_node(p_state["previewing"]); @@ -4063,7 +4118,7 @@ void Node3DEditorViewport::set_state(const Dictionary &p_state) { preview_camera->show(); } } - preview_camera->connect("toggled", callable_mp(this, &Node3DEditorViewport::_toggle_camera_preview)); + preview_camera->connect(SceneStringName(toggled), callable_mp(this, &Node3DEditorViewport::_toggle_camera_preview)); } Dictionary Node3DEditorViewport::get_state() const { @@ -4097,6 +4152,7 @@ Dictionary Node3DEditorViewport::get_state() const { d["listener"] = viewport->is_audio_listener_3d(); d["doppler"] = view_menu->get_popup()->is_item_checked(view_menu->get_popup()->get_item_index(VIEW_AUDIO_DOPPLER)); d["gizmos"] = view_menu->get_popup()->is_item_checked(view_menu->get_popup()->get_item_index(VIEW_GIZMOS)); + d["transform_gizmo"] = view_menu->get_popup()->is_item_checked(view_menu->get_popup()->get_item_index(VIEW_TRANSFORM_GIZMO)); d["grid"] = view_menu->get_popup()->is_item_checked(view_menu->get_popup()->get_item_index(VIEW_GRID)); d["information"] = view_menu->get_popup()->is_item_checked(view_menu->get_popup()->get_item_index(VIEW_INFORMATION)); d["frame_time"] = view_menu->get_popup()->is_item_checked(view_menu->get_popup()->get_item_index(VIEW_FRAME_TIME)); @@ -5330,6 +5386,7 @@ Node3DEditorViewport::Node3DEditorViewport(Node3DEditor *p_spatial_editor, int p view_menu->get_popup()->add_separator(); view_menu->get_popup()->add_check_shortcut(ED_SHORTCUT("spatial_editor/view_environment", TTR("View Environment")), VIEW_ENVIRONMENT); view_menu->get_popup()->add_check_shortcut(ED_SHORTCUT("spatial_editor/view_gizmos", TTR("View Gizmos")), VIEW_GIZMOS); + view_menu->get_popup()->add_check_shortcut(ED_SHORTCUT("spatial_editor/view_transform_gizmo", TTR("View Transform Gizmo")), VIEW_TRANSFORM_GIZMO); view_menu->get_popup()->add_check_shortcut(ED_SHORTCUT("spatial_editor/view_grid_lines", TTR("View Grid")), VIEW_GRID); view_menu->get_popup()->add_check_shortcut(ED_SHORTCUT("spatial_editor/view_information", TTR("View Information")), VIEW_INFORMATION); view_menu->get_popup()->add_check_shortcut(ED_SHORTCUT("spatial_editor/view_fps", TTR("View Frame Time")), VIEW_FRAME_TIME); @@ -5340,6 +5397,7 @@ Node3DEditorViewport::Node3DEditorViewport(Node3DEditor *p_spatial_editor, int p view_menu->get_popup()->add_check_shortcut(ED_SHORTCUT("spatial_editor/view_audio_listener", TTR("Audio Listener")), VIEW_AUDIO_LISTENER); view_menu->get_popup()->add_check_shortcut(ED_SHORTCUT("spatial_editor/view_audio_doppler", TTR("Enable Doppler")), VIEW_AUDIO_DOPPLER); view_menu->get_popup()->set_item_checked(view_menu->get_popup()->get_item_index(VIEW_GIZMOS), true); + view_menu->get_popup()->set_item_checked(view_menu->get_popup()->get_item_index(VIEW_TRANSFORM_GIZMO), true); view_menu->get_popup()->set_item_checked(view_menu->get_popup()->get_item_index(VIEW_GRID), true); view_menu->get_popup()->add_separator(); @@ -5374,6 +5432,14 @@ Node3DEditorViewport::Node3DEditorViewport(Node3DEditor *p_spatial_editor, int p view_menu->get_popup()->set_item_tooltip(shadeless_idx, unsupported_tooltip); } + // Registering with Key::NONE intentionally creates an empty Array. + register_shortcut_action("spatial_editor/viewport_orbit_modifier_1", TTR("Viewport Orbit Modifier 1"), Key::NONE); + register_shortcut_action("spatial_editor/viewport_orbit_modifier_2", TTR("Viewport Orbit Modifier 2"), Key::NONE); + register_shortcut_action("spatial_editor/viewport_pan_modifier_1", TTR("Viewport Pan Modifier 1"), Key::SHIFT); + register_shortcut_action("spatial_editor/viewport_pan_modifier_2", TTR("Viewport Pan Modifier 2"), Key::NONE); + register_shortcut_action("spatial_editor/viewport_zoom_modifier_1", TTR("Viewport Zoom Modifier 1"), Key::SHIFT); + register_shortcut_action("spatial_editor/viewport_zoom_modifier_2", TTR("Viewport Zoom Modifier 2"), Key::CTRL); + register_shortcut_action("spatial_editor/freelook_left", TTR("Freelook Left"), Key::A, true); register_shortcut_action("spatial_editor/freelook_right", TTR("Freelook Right"), Key::D, true); register_shortcut_action("spatial_editor/freelook_forward", TTR("Freelook Forward"), Key::W, true); @@ -5400,7 +5466,7 @@ Node3DEditorViewport::Node3DEditorViewport(Node3DEditor *p_spatial_editor, int p vbox->add_child(preview_camera); preview_camera->set_h_size_flags(0); preview_camera->hide(); - preview_camera->connect("toggled", callable_mp(this, &Node3DEditorViewport::_toggle_camera_preview)); + preview_camera->connect(SceneStringName(toggled), callable_mp(this, &Node3DEditorViewport::_toggle_camera_preview)); previewing = nullptr; gizmo_scale = 1.0; @@ -8615,7 +8681,7 @@ Node3DEditor::Node3DEditor() { main_menu_hbox->add_child(tool_option_button[TOOL_OPT_LOCAL_COORDS]); tool_option_button[TOOL_OPT_LOCAL_COORDS]->set_toggle_mode(true); tool_option_button[TOOL_OPT_LOCAL_COORDS]->set_theme_type_variation("FlatButton"); - tool_option_button[TOOL_OPT_LOCAL_COORDS]->connect("toggled", callable_mp(this, &Node3DEditor::_menu_item_toggled).bind(MENU_TOOL_LOCAL_COORDS)); + tool_option_button[TOOL_OPT_LOCAL_COORDS]->connect(SceneStringName(toggled), callable_mp(this, &Node3DEditor::_menu_item_toggled).bind(MENU_TOOL_LOCAL_COORDS)); tool_option_button[TOOL_OPT_LOCAL_COORDS]->set_shortcut(ED_SHORTCUT("spatial_editor/local_coords", TTR("Use Local Space"), Key::T)); tool_option_button[TOOL_OPT_LOCAL_COORDS]->set_shortcut_context(this); @@ -8623,7 +8689,7 @@ Node3DEditor::Node3DEditor() { main_menu_hbox->add_child(tool_option_button[TOOL_OPT_USE_SNAP]); tool_option_button[TOOL_OPT_USE_SNAP]->set_toggle_mode(true); tool_option_button[TOOL_OPT_USE_SNAP]->set_theme_type_variation("FlatButton"); - tool_option_button[TOOL_OPT_USE_SNAP]->connect("toggled", callable_mp(this, &Node3DEditor::_menu_item_toggled).bind(MENU_TOOL_USE_SNAP)); + tool_option_button[TOOL_OPT_USE_SNAP]->connect(SceneStringName(toggled), callable_mp(this, &Node3DEditor::_menu_item_toggled).bind(MENU_TOOL_USE_SNAP)); tool_option_button[TOOL_OPT_USE_SNAP]->set_shortcut(ED_SHORTCUT("spatial_editor/snap", TTR("Use Snap"), Key::Y)); tool_option_button[TOOL_OPT_USE_SNAP]->set_shortcut_context(this); @@ -8634,7 +8700,7 @@ Node3DEditor::Node3DEditor() { tool_option_button[TOOL_OPT_OVERRIDE_CAMERA]->set_toggle_mode(true); tool_option_button[TOOL_OPT_OVERRIDE_CAMERA]->set_theme_type_variation("FlatButton"); tool_option_button[TOOL_OPT_OVERRIDE_CAMERA]->set_disabled(true); - tool_option_button[TOOL_OPT_OVERRIDE_CAMERA]->connect("toggled", callable_mp(this, &Node3DEditor::_menu_item_toggled).bind(MENU_TOOL_OVERRIDE_CAMERA)); + tool_option_button[TOOL_OPT_OVERRIDE_CAMERA]->connect(SceneStringName(toggled), callable_mp(this, &Node3DEditor::_menu_item_toggled).bind(MENU_TOOL_OVERRIDE_CAMERA)); _update_camera_override_button(false); main_menu_hbox->add_child(memnew(VSeparator)); diff --git a/editor/plugins/node_3d_editor_plugin.h b/editor/plugins/node_3d_editor_plugin.h index 9e7d46c5e8..2cfe784ca6 100644 --- a/editor/plugins/node_3d_editor_plugin.h +++ b/editor/plugins/node_3d_editor_plugin.h @@ -125,6 +125,7 @@ class Node3DEditorViewport : public Control { VIEW_AUDIO_LISTENER, VIEW_AUDIO_DOPPLER, VIEW_GIZMOS, + VIEW_TRANSFORM_GIZMO, VIEW_GRID, VIEW_INFORMATION, VIEW_FRAME_TIME, @@ -192,6 +193,7 @@ public: NAVIGATION_GODOT, NAVIGATION_MAYA, NAVIGATION_MODO, + NAVIGATION_CUSTOM, }; enum FreelookNavigationScheme { @@ -200,6 +202,12 @@ public: FREELOOK_FULLY_AXIS_LOCKED, }; + enum ViewportNavMouseButton { + NAVIGATION_LEFT_MOUSE, + NAVIGATION_MIDDLE_MOUSE, + NAVIGATION_RIGHT_MOUSE, + }; + private: double cpu_time_history[FRAME_TIME_HISTORY]; int cpu_time_history_index; @@ -236,6 +244,7 @@ private: bool orthogonal; bool auto_orthogonal; bool lock_rotation; + bool transform_gizmo_visible = true; real_t gizmo_scale; bool freelook_active; @@ -294,6 +303,10 @@ private: void _nav_orbit(Ref<InputEventWithModifiers> p_event, const Vector2 &p_relative); void _nav_look(Ref<InputEventWithModifiers> p_event, const Vector2 &p_relative); + bool _is_shortcut_empty(const String &p_name); + bool _is_nav_modifier_pressed(const String &p_name); + int _get_shortcut_input_count(const String &p_name); + float get_znear() const; float get_zfar() const; float get_fov() const; @@ -390,6 +403,28 @@ private: void reset_fov(); void scale_cursor_distance(real_t scale); + struct ShortcutCheckSet { + bool mod_pressed = false; + bool shortcut_not_empty = true; + int input_count = 0; + ViewportNavMouseButton mouse_preference = NAVIGATION_LEFT_MOUSE; + NavigationMode result_nav_mode = NAVIGATION_NONE; + + ShortcutCheckSet() {} + + ShortcutCheckSet(bool p_mod_pressed, bool p_shortcut_not_empty, int p_input_count, const ViewportNavMouseButton &p_mouse_preference, const NavigationMode &p_result_nav_mode) : + mod_pressed(p_mod_pressed), shortcut_not_empty(p_shortcut_not_empty), input_count(p_input_count), mouse_preference(p_mouse_preference), result_nav_mode(p_result_nav_mode) { + } + }; + + struct ShortcutCheckSetComparator { + _FORCE_INLINE_ bool operator()(const ShortcutCheckSet &A, const ShortcutCheckSet &B) const { + return A.input_count > B.input_count; + } + }; + + NavigationMode _get_nav_mode_from_shortcut_check(ViewportNavMouseButton p_mouse_button, Vector<ShortcutCheckSet> p_shortcut_check_sets, bool p_use_not_empty); + void set_freelook_active(bool active_now); void scale_freelook_speed(real_t scale); diff --git a/editor/plugins/particle_process_material_editor_plugin.cpp b/editor/plugins/particle_process_material_editor_plugin.cpp index 79c9c69584..67c9403aaf 100644 --- a/editor/plugins/particle_process_material_editor_plugin.cpp +++ b/editor/plugins/particle_process_material_editor_plugin.cpp @@ -438,7 +438,7 @@ ParticleProcessMaterialMinMaxPropertyEditor::ParticleProcessMaterialMinMaxProper toggle_mode_button->set_toggle_mode(true); toggle_mode_button->set_tooltip_text(TTR("Toggle between minimum/maximum and base value/spread modes.")); hb->add_child(toggle_mode_button); - toggle_mode_button->connect(SNAME("toggled"), callable_mp(this, &ParticleProcessMaterialMinMaxPropertyEditor::_toggle_mode)); + toggle_mode_button->connect(SceneStringName(toggled), callable_mp(this, &ParticleProcessMaterialMinMaxPropertyEditor::_toggle_mode)); set_bottom_editor(content_vb); } diff --git a/editor/plugins/physical_bone_3d_editor_plugin.cpp b/editor/plugins/physical_bone_3d_editor_plugin.cpp index d7e9701452..c858fa8606 100644 --- a/editor/plugins/physical_bone_3d_editor_plugin.cpp +++ b/editor/plugins/physical_bone_3d_editor_plugin.cpp @@ -62,7 +62,7 @@ PhysicalBone3DEditor::PhysicalBone3DEditor() { // when the editor theme updates. button_transform_joint->set_icon(EditorNode::get_singleton()->get_editor_theme()->get_icon(SNAME("PhysicalBone3D"), EditorStringName(EditorIcons))); button_transform_joint->set_toggle_mode(true); - button_transform_joint->connect("toggled", callable_mp(this, &PhysicalBone3DEditor::_on_toggle_button_transform_joint)); + button_transform_joint->connect(SceneStringName(toggled), callable_mp(this, &PhysicalBone3DEditor::_on_toggle_button_transform_joint)); hide(); } diff --git a/editor/plugins/polygon_2d_editor_plugin.cpp b/editor/plugins/polygon_2d_editor_plugin.cpp index f0ea322504..b8a309bc60 100644 --- a/editor/plugins/polygon_2d_editor_plugin.cpp +++ b/editor/plugins/polygon_2d_editor_plugin.cpp @@ -181,7 +181,7 @@ void Polygon2DEditor::_sync_bones() { } if (weights.size() == 0) { //create them - weights.resize(node->get_polygon().size()); + weights.resize(wc); float *w = weights.ptrw(); for (int j = 0; j < wc; j++) { w[j] = 0.0; @@ -850,8 +850,8 @@ void Polygon2DEditor::_uv_input(const Ref<InputEvent> &p_input) { if (mm.is_valid()) { if (uv_drag) { Vector2 uv_drag_to = mm->get_position(); - uv_drag_to = snap_point(uv_drag_to); // FIXME: Only works correctly with 'UV_MODE_EDIT_POINT', it's imprecise with the rest. - Vector2 drag = mtx.affine_inverse().xform(uv_drag_to) - mtx.affine_inverse().xform(uv_drag_from); + uv_drag_to = snap_point(uv_drag_to); + Vector2 drag = mtx.affine_inverse().basis_xform(uv_drag_to - uv_drag_from); switch (uv_move_current) { case UV_MODE_CREATE: { @@ -1166,12 +1166,8 @@ void Polygon2DEditor::_uv_draw() { poly_line_color.a *= 0.25; } Color polygon_line_color = Color(0.5, 0.5, 0.9); - Vector<Color> polygon_fill_color; - { - Color pf = polygon_line_color; - pf.a *= 0.5; - polygon_fill_color.push_back(pf); - } + Color polygon_fill_color = polygon_line_color; + polygon_fill_color.a *= 0.5; Color prev_color = Color(0.5, 0.5, 0.5); int uv_draw_max = uvs.size(); @@ -1216,7 +1212,7 @@ void Polygon2DEditor::_uv_draw() { uv_edit_draw->draw_line(mtx.xform(uvs[idx]), mtx.xform(uvs[idx_next]), polygon_line_color, Math::round(EDSCALE)); } if (points.size() >= 3) { - uv_edit_draw->draw_polygon(polypoints, polygon_fill_color); + uv_edit_draw->draw_colored_polygon(polypoints, polygon_fill_color); } } @@ -1308,8 +1304,8 @@ void Polygon2DEditor::_bind_methods() { Vector2 Polygon2DEditor::snap_point(Vector2 p_target) const { if (use_snap) { - p_target.x = Math::snap_scalar(snap_offset.x * uv_draw_zoom - uv_draw_ofs.x, snap_step.x * uv_draw_zoom, p_target.x); - p_target.y = Math::snap_scalar(snap_offset.y * uv_draw_zoom - uv_draw_ofs.y, snap_step.y * uv_draw_zoom, p_target.y); + p_target.x = Math::snap_scalar((snap_offset.x - uv_draw_ofs.x) * uv_draw_zoom, snap_step.x * uv_draw_zoom, p_target.x); + p_target.y = Math::snap_scalar((snap_offset.y - uv_draw_ofs.y) * uv_draw_zoom, snap_step.y * uv_draw_zoom, p_target.y); } return p_target; @@ -1387,7 +1383,8 @@ Polygon2DEditor::Polygon2DEditor() { uv_button[UV_MODE_CREATE_INTERNAL]->set_tooltip_text(TTR("Create Internal Vertex")); uv_button[UV_MODE_REMOVE_INTERNAL]->set_tooltip_text(TTR("Remove Internal Vertex")); Key key = (OS::get_singleton()->has_feature("macos") || OS::get_singleton()->has_feature("web_macos") || OS::get_singleton()->has_feature("web_ios")) ? Key::META : Key::CTRL; - uv_button[UV_MODE_EDIT_POINT]->set_tooltip_text(TTR("Move Points") + "\n" + find_keycode_name(key) + TTR(": Rotate") + "\n" + TTR("Shift: Move All") + "\n" + keycode_get_string((Key)KeyModifierMask::CMD_OR_CTRL) + TTR("Shift: Scale")); + // TRANSLATORS: %s is Control or Command key name. + uv_button[UV_MODE_EDIT_POINT]->set_tooltip_text(TTR("Move Points") + "\n" + vformat(TTR("%s: Rotate"), find_keycode_name(key)) + "\n" + TTR("Shift: Move All") + "\n" + vformat(TTR("%s + Shift: Scale"), find_keycode_name(key))); uv_button[UV_MODE_MOVE]->set_tooltip_text(TTR("Move Polygon")); uv_button[UV_MODE_ROTATE]->set_tooltip_text(TTR("Rotate Polygon")); uv_button[UV_MODE_SCALE]->set_tooltip_text(TTR("Scale Polygon")); @@ -1471,7 +1468,7 @@ Polygon2DEditor::Polygon2DEditor() { b_snap_enable->set_toggle_mode(true); b_snap_enable->set_pressed(use_snap); b_snap_enable->set_tooltip_text(TTR("Enable Snap")); - b_snap_enable->connect("toggled", callable_mp(this, &Polygon2DEditor::_set_use_snap)); + b_snap_enable->connect(SceneStringName(toggled), callable_mp(this, &Polygon2DEditor::_set_use_snap)); b_snap_grid = memnew(Button); b_snap_grid->set_theme_type_variation("FlatButton"); @@ -1481,11 +1478,11 @@ Polygon2DEditor::Polygon2DEditor() { b_snap_grid->set_toggle_mode(true); b_snap_grid->set_pressed(snap_show_grid); b_snap_grid->set_tooltip_text(TTR("Show Grid")); - b_snap_grid->connect("toggled", callable_mp(this, &Polygon2DEditor::_set_show_grid)); + b_snap_grid->connect(SceneStringName(toggled), callable_mp(this, &Polygon2DEditor::_set_show_grid)); grid_settings = memnew(AcceptDialog); grid_settings->set_title(TTR("Configure Grid:")); - add_child(grid_settings); + uv_edit->add_child(grid_settings); VBoxContainer *grid_settings_vb = memnew(VBoxContainer); grid_settings->add_child(grid_settings_vb); diff --git a/editor/plugins/script_editor_plugin.cpp b/editor/plugins/script_editor_plugin.cpp index 9da66a0862..d7de5a7223 100644 --- a/editor/plugins/script_editor_plugin.cpp +++ b/editor/plugins/script_editor_plugin.cpp @@ -493,7 +493,7 @@ void ScriptEditor::_goto_script_line(Ref<RefCounted> p_script, int p_line) { if (ScriptTextEditor *script_text_editor = Object::cast_to<ScriptTextEditor>(current)) { script_text_editor->goto_line_centered(p_line); } else if (current) { - current->goto_line(p_line, true); + current->goto_line(p_line); } _save_history(); @@ -578,8 +578,8 @@ void ScriptEditor::_clear_breakpoints() { script_editor_cache->get_sections(&cached_editors); for (const String &E : cached_editors) { Array breakpoints = _get_cached_breakpoints_for_script(E); - for (int i = 0; i < breakpoints.size(); i++) { - EditorDebuggerNode::get_singleton()->set_breakpoint(E, (int)breakpoints[i] + 1, false); + for (int breakpoint : breakpoints) { + EditorDebuggerNode::get_singleton()->set_breakpoint(E, (int)breakpoint + 1, false); } if (breakpoints.size() > 0) { @@ -1079,8 +1079,12 @@ void ScriptEditor::_mark_built_in_scripts_as_saved(const String &p_parent_path) } Ref<Script> scr = edited_res; - if (scr.is_valid() && scr->is_tool()) { - scr->reload(true); + if (scr.is_valid()) { + trigger_live_script_reload(scr->get_path()); + + if (scr->is_tool()) { + scr->reload(true); + } } } } @@ -1816,6 +1820,48 @@ void ScriptEditor::notify_script_changed(const Ref<Script> &p_script) { emit_signal(SNAME("editor_script_changed"), p_script); } +Vector<String> ScriptEditor::_get_breakpoints() { + Vector<String> ret; + HashSet<String> loaded_scripts; + for (int i = 0; i < tab_container->get_tab_count(); i++) { + ScriptEditorBase *se = Object::cast_to<ScriptEditorBase>(tab_container->get_tab_control(i)); + if (!se) { + continue; + } + + Ref<Script> scr = se->get_edited_resource(); + if (scr.is_null()) { + continue; + } + + String base = scr->get_path(); + loaded_scripts.insert(base); + if (base.is_empty() || base.begins_with("local://")) { + continue; + } + + PackedInt32Array bpoints = se->get_breakpoints(); + for (int32_t bpoint : bpoints) { + ret.push_back(base + ":" + itos((int)bpoint + 1)); + } + } + + // Load breakpoints that are in closed scripts. + List<String> cached_editors; + script_editor_cache->get_sections(&cached_editors); + for (const String &E : cached_editors) { + if (loaded_scripts.has(E)) { + continue; + } + + Array breakpoints = _get_cached_breakpoints_for_script(E); + for (int breakpoint : breakpoints) { + ret.push_back(E + ":" + itos((int)breakpoint + 1)); + } + } + return ret; +} + void ScriptEditor::get_breakpoints(List<String> *p_breakpoints) { HashSet<String> loaded_scripts; for (int i = 0; i < tab_container->get_tab_count(); i++) { @@ -1825,19 +1871,19 @@ void ScriptEditor::get_breakpoints(List<String> *p_breakpoints) { } Ref<Script> scr = se->get_edited_resource(); - if (scr == nullptr) { + if (scr.is_null()) { continue; } String base = scr->get_path(); loaded_scripts.insert(base); - if (base.begins_with("local://") || base.is_empty()) { + if (base.is_empty() || base.begins_with("local://")) { continue; } PackedInt32Array bpoints = se->get_breakpoints(); - for (int j = 0; j < bpoints.size(); j++) { - p_breakpoints->push_back(base + ":" + itos((int)bpoints[j] + 1)); + for (int32_t bpoint : bpoints) { + p_breakpoints->push_back(base + ":" + itos((int)bpoint + 1)); } } @@ -1850,24 +1896,20 @@ void ScriptEditor::get_breakpoints(List<String> *p_breakpoints) { } Array breakpoints = _get_cached_breakpoints_for_script(E); - for (int i = 0; i < breakpoints.size(); i++) { - p_breakpoints->push_back(E + ":" + itos((int)breakpoints[i] + 1)); + for (int breakpoint : breakpoints) { + p_breakpoints->push_back(E + ":" + itos((int)breakpoint + 1)); } } } void ScriptEditor::_members_overview_selected(int p_idx) { - ScriptEditorBase *se = _get_current_editor(); - if (!se) { - return; + int line = members_overview->get_item_metadata(p_idx); + ScriptEditorBase *current = _get_current_editor(); + if (ScriptTextEditor *script_text_editor = Object::cast_to<ScriptTextEditor>(current)) { + script_text_editor->goto_line_centered(line); + } else if (current) { + current->goto_line(line); } - // Go to the member's line and reset the cursor column. We can't change scroll_position - // directly until we have gone to the line first, since code might be folded. - se->goto_line(members_overview->get_item_metadata(p_idx)); - Dictionary state = se->get_edit_state(); - state["column"] = 0; - state["scroll_position"] = members_overview->get_item_metadata(p_idx); - se->set_edit_state(state); } void ScriptEditor::_help_overview_selected(int p_idx) { @@ -2711,9 +2753,11 @@ void ScriptEditor::apply_scripts() const { } void ScriptEditor::reload_scripts(bool p_refresh_only) { - if (external_editor_active) { - return; - } + // Call deferred to make sure it runs on the main thread. + callable_mp(this, &ScriptEditor::_reload_scripts).call_deferred(p_refresh_only); +} + +void ScriptEditor::_reload_scripts(bool p_refresh_only) { for (int i = 0; i < tab_container->get_tab_count(); i++) { ScriptEditorBase *se = Object::cast_to<ScriptEditorBase>(tab_container->get_tab_control(i)); if (!se) { @@ -2942,8 +2986,8 @@ void ScriptEditor::_files_moved(const String &p_old_file, const String &p_new_fi // If Script, update breakpoints with debugger. Array breakpoints = _get_cached_breakpoints_for_script(p_new_file); - for (int i = 0; i < breakpoints.size(); i++) { - int line = (int)breakpoints[i] + 1; + for (int breakpoint : breakpoints) { + int line = (int)breakpoint + 1; EditorDebuggerNode::get_singleton()->set_breakpoint(p_old_file, line, false); if (!p_new_file.begins_with("local://") && ResourceLoader::exists(p_new_file, "Script")) { EditorDebuggerNode::get_singleton()->set_breakpoint(p_new_file, line, true); @@ -2967,8 +3011,8 @@ void ScriptEditor::_file_removed(const String &p_removed_file) { // Check closed. if (script_editor_cache->has_section(p_removed_file)) { Array breakpoints = _get_cached_breakpoints_for_script(p_removed_file); - for (int i = 0; i < breakpoints.size(); i++) { - EditorDebuggerNode::get_singleton()->set_breakpoint(p_removed_file, (int)breakpoints[i] + 1, false); + for (int breakpoint : breakpoints) { + EditorDebuggerNode::get_singleton()->set_breakpoint(p_removed_file, (int)breakpoint + 1, false); } script_editor_cache->erase_section(p_removed_file); } @@ -3423,8 +3467,8 @@ void ScriptEditor::set_window_layout(Ref<ConfigFile> p_layout) { } Array breakpoints = _get_cached_breakpoints_for_script(E); - for (int i = 0; i < breakpoints.size(); i++) { - EditorDebuggerNode::get_singleton()->set_breakpoint(E, (int)breakpoints[i] + 1, true); + for (int breakpoint : breakpoints) { + EditorDebuggerNode::get_singleton()->set_breakpoint(E, (int)breakpoint + 1, true); } } @@ -3970,6 +4014,7 @@ void ScriptEditor::_bind_methods() { ClassDB::bind_method("_help_tab_goto", &ScriptEditor::_help_tab_goto); ClassDB::bind_method("get_current_editor", &ScriptEditor::_get_current_editor); ClassDB::bind_method("get_open_script_editors", &ScriptEditor::_get_open_script_editors); + ClassDB::bind_method("get_breakpoints", &ScriptEditor::_get_breakpoints); ClassDB::bind_method(D_METHOD("register_syntax_highlighter", "syntax_highlighter"), &ScriptEditor::register_syntax_highlighter); ClassDB::bind_method(D_METHOD("unregister_syntax_highlighter", "syntax_highlighter"), &ScriptEditor::unregister_syntax_highlighter); @@ -4061,7 +4106,7 @@ ScriptEditor::ScriptEditor(WindowWrapper *p_wrapper) { members_overview_alphabeta_sort_button->set_tooltip_text(TTR("Toggle alphabetical sorting of the method list.")); members_overview_alphabeta_sort_button->set_toggle_mode(true); members_overview_alphabeta_sort_button->set_pressed(EDITOR_GET("text_editor/script_list/sort_members_outline_alphabetically")); - members_overview_alphabeta_sort_button->connect("toggled", callable_mp(this, &ScriptEditor::_toggle_members_overview_alpha_sort)); + members_overview_alphabeta_sort_button->connect(SceneStringName(toggled), callable_mp(this, &ScriptEditor::_toggle_members_overview_alpha_sort)); buttons_hbox->add_child(members_overview_alphabeta_sort_button); @@ -4415,13 +4460,9 @@ bool ScriptEditorPlugin::handles(Object *p_object) const { void ScriptEditorPlugin::make_visible(bool p_visible) { if (p_visible) { window_wrapper->show(); - script_editor->set_process(true); script_editor->ensure_select_current(); } else { window_wrapper->hide(); - if (!window_wrapper->get_window_enabled()) { - script_editor->set_process(false); - } } } diff --git a/editor/plugins/script_editor_plugin.h b/editor/plugins/script_editor_plugin.h index 9db1aff76a..e0fac5d0c6 100644 --- a/editor/plugins/script_editor_plugin.h +++ b/editor/plugins/script_editor_plugin.h @@ -55,7 +55,7 @@ class EditorSyntaxHighlighter : public SyntaxHighlighter { GDCLASS(EditorSyntaxHighlighter, SyntaxHighlighter) private: - Ref<RefCounted> edited_resourse; + Ref<RefCounted> edited_resource; protected: static void _bind_methods(); @@ -67,8 +67,8 @@ public: virtual String _get_name() const; virtual PackedStringArray _get_supported_languages() const; - void _set_edited_resource(const Ref<Resource> &p_res) { edited_resourse = p_res; } - Ref<RefCounted> _get_edited_resource() { return edited_resourse; } + void _set_edited_resource(const Ref<Resource> &p_res) { edited_resource = p_res; } + Ref<RefCounted> _get_edited_resource() { return edited_resource; } virtual Ref<EditorSyntaxHighlighter> _create() const; }; @@ -170,7 +170,7 @@ public: virtual Variant get_edit_state() = 0; virtual void set_edit_state(const Variant &p_state) = 0; virtual Variant get_navigation_state() = 0; - virtual void goto_line(int p_line, bool p_with_error = false) = 0; + virtual void goto_line(int p_line, int p_column = 0) = 0; virtual void set_executing_line(int p_line) = 0; virtual void clear_executing_line() = 0; virtual void trim_trailing_whitespace() = 0; @@ -436,6 +436,7 @@ class ScriptEditor : public PanelContainer { void _file_removed(const String &p_file); void _autosave_scripts(); void _update_autosave_timer(); + void _reload_scripts(bool p_refresh_only = false); void _update_members_overview_visibility(); void _update_members_overview(); @@ -538,6 +539,7 @@ public: _FORCE_INLINE_ bool edit(const Ref<Resource> &p_resource, bool p_grab_focus = true) { return edit(p_resource, -1, 0, p_grab_focus); } bool edit(const Ref<Resource> &p_resource, int p_line, int p_col, bool p_grab_focus = true); + Vector<String> _get_breakpoints(); void get_breakpoints(List<String> *p_breakpoints); PackedStringArray get_unsaved_scripts() const; diff --git a/editor/plugins/script_text_editor.cpp b/editor/plugins/script_text_editor.cpp index 070471f3f3..34557b26b4 100644 --- a/editor/plugins/script_text_editor.cpp +++ b/editor/plugins/script_text_editor.cpp @@ -302,16 +302,14 @@ void ScriptTextEditor::_warning_clicked(const Variant &p_line) { void ScriptTextEditor::_error_clicked(const Variant &p_line) { if (p_line.get_type() == Variant::INT) { - code_editor->get_text_editor()->remove_secondary_carets(); - code_editor->get_text_editor()->set_caret_line(p_line.operator int64_t()); + goto_line_centered(p_line.operator int64_t()); } else if (p_line.get_type() == Variant::DICTIONARY) { Dictionary meta = p_line.operator Dictionary(); const String path = meta["path"].operator String(); const int line = meta["line"].operator int64_t(); const int column = meta["column"].operator int64_t(); if (path.is_empty()) { - code_editor->get_text_editor()->remove_secondary_carets(); - code_editor->get_text_editor()->set_caret_line(line); + goto_line_centered(line, column); } else { Ref<Resource> scr = ResourceLoader::load(path); if (!scr.is_valid()) { @@ -456,16 +454,16 @@ void ScriptTextEditor::tag_saved_version() { code_editor->get_text_editor()->tag_saved_version(); } -void ScriptTextEditor::goto_line(int p_line, bool p_with_error) { - code_editor->goto_line(p_line); +void ScriptTextEditor::goto_line(int p_line, int p_column) { + code_editor->goto_line(p_line, p_column); } void ScriptTextEditor::goto_line_selection(int p_line, int p_begin, int p_end) { code_editor->goto_line_selection(p_line, p_begin, p_end); } -void ScriptTextEditor::goto_line_centered(int p_line) { - code_editor->goto_line_centered(p_line); +void ScriptTextEditor::goto_line_centered(int p_line, int p_column) { + code_editor->goto_line_centered(p_line, p_column); } void ScriptTextEditor::set_executing_line(int p_line) { @@ -919,8 +917,7 @@ void ScriptTextEditor::_breakpoint_item_pressed(int p_idx) { if (p_idx < 4) { // Any item before the separator. _edit_option(breakpoints_menu->get_item_id(p_idx)); } else { - code_editor->goto_line(breakpoints_menu->get_item_metadata(p_idx)); - callable_mp((TextEdit *)code_editor->get_text_editor(), &TextEdit::center_viewport_to_caret).call_deferred(0); // Needs to be deferred, because goto uses call_deferred(). + code_editor->goto_line_centered(breakpoints_menu->get_item_metadata(p_idx)); } } @@ -1816,9 +1813,9 @@ static String _get_dropped_resource_line(const Ref<Resource> &p_resource, bool p } if (is_script) { - variable_name = variable_name.to_pascal_case().validate_identifier(); + variable_name = variable_name.to_pascal_case().validate_ascii_identifier(); } else { - variable_name = variable_name.to_snake_case().to_upper().validate_identifier(); + variable_name = variable_name.to_snake_case().to_upper().validate_ascii_identifier(); } return vformat("const %s = preload(%s)", variable_name, _quote_drop_data(path)); } @@ -1932,13 +1929,13 @@ void ScriptTextEditor::drop_data_fw(const Point2 &p_point, const Variant &p_data path = sn->get_path_to(node); } for (const String &segment : path.split("/")) { - if (!segment.is_valid_identifier()) { + if (!segment.is_valid_ascii_identifier()) { path = _quote_drop_data(path); break; } } - String variable_name = String(node->get_name()).to_snake_case().validate_identifier(); + String variable_name = String(node->get_name()).to_snake_case().validate_ascii_identifier(); if (use_type) { StringName class_name = node->get_class_name(); Ref<Script> node_script = node->get_script(); @@ -1975,7 +1972,7 @@ void ScriptTextEditor::drop_data_fw(const Point2 &p_point, const Variant &p_data } for (const String &segment : path.split("/")) { - if (!segment.is_valid_identifier()) { + if (!segment.is_valid_ascii_identifier()) { path = _quote_drop_data(path); break; } @@ -2376,6 +2373,7 @@ void ScriptTextEditor::_enable_code_editor() { ScriptTextEditor::ScriptTextEditor() { code_editor = memnew(CodeTextEditor); + code_editor->set_toggle_list_control(ScriptEditor::get_singleton()->get_left_list_split()); code_editor->add_theme_constant_override("separation", 2); code_editor->set_anchors_and_offsets_preset(Control::PRESET_FULL_RECT); code_editor->set_code_complete_func(_code_complete_scripts, this); diff --git a/editor/plugins/script_text_editor.h b/editor/plugins/script_text_editor.h index 8c2ec1561b..7aa0726479 100644 --- a/editor/plugins/script_text_editor.h +++ b/editor/plugins/script_text_editor.h @@ -234,9 +234,9 @@ public: virtual void convert_indent() override; virtual void tag_saved_version() override; - virtual void goto_line(int p_line, bool p_with_error = false) override; + virtual void goto_line(int p_line, int p_column = 0) override; void goto_line_selection(int p_line, int p_begin, int p_end); - void goto_line_centered(int p_line); + void goto_line_centered(int p_line, int p_column = 0); virtual void set_executing_line(int p_line) override; virtual void clear_executing_line() override; diff --git a/editor/plugins/shader_editor_plugin.cpp b/editor/plugins/shader_editor_plugin.cpp index 82c436d3a5..ea049756b7 100644 --- a/editor/plugins/shader_editor_plugin.cpp +++ b/editor/plugins/shader_editor_plugin.cpp @@ -45,6 +45,16 @@ #include "scene/gui/item_list.h" #include "scene/gui/texture_rect.h" +Ref<Resource> ShaderEditorPlugin::_get_current_shader() { + int index = shader_tabs->get_current_tab(); + ERR_FAIL_INDEX_V(index, shader_tabs->get_tab_count(), Ref<Resource>()); + if (edited_shaders[index].shader.is_valid()) { + return edited_shaders[index].shader; + } else { + return edited_shaders[index].shader_inc; + } +} + void ShaderEditorPlugin::_update_shader_list() { shader_list->clear(); for (EditedShader &edited_shader : edited_shaders) { @@ -93,9 +103,7 @@ void ShaderEditorPlugin::_update_shader_list() { shader_list->select(shader_tabs->get_current_tab()); } - for (int i = FILE_SAVE; i < FILE_MAX; i++) { - file_menu->get_popup()->set_item_disabled(file_menu->get_popup()->get_item_index(i), edited_shaders.is_empty()); - } + _set_file_specific_items_disabled(edited_shaders.is_empty()); _update_shader_list_status(); } @@ -141,7 +149,9 @@ void ShaderEditorPlugin::edit(Object *p_object) { } } es.shader_inc = Ref<ShaderInclude>(si); - es.shader_editor = memnew(TextShaderEditor); + TextShaderEditor *text_shader = memnew(TextShaderEditor); + text_shader->get_code_editor()->set_toggle_list_control(left_panel); + es.shader_editor = text_shader; es.shader_editor->edit_shader_include(si); shader_tabs->add_child(es.shader_editor); } else { @@ -158,7 +168,9 @@ void ShaderEditorPlugin::edit(Object *p_object) { if (vs.is_valid()) { es.shader_editor = memnew(VisualShaderEditor); } else { - es.shader_editor = memnew(TextShaderEditor); + TextShaderEditor *text_shader = memnew(TextShaderEditor); + text_shader->get_code_editor()->set_toggle_list_control(left_panel); + es.shader_editor = text_shader; } shader_tabs->add_child(es.shader_editor); es.shader_editor->edit_shader(es.shader); @@ -362,6 +374,61 @@ void ShaderEditorPlugin::_shader_list_clicked(int p_item, Vector2 p_local_mouse_ if (p_mouse_button_index == MouseButton::MIDDLE) { _close_shader(p_item); } + if (p_mouse_button_index == MouseButton::RIGHT) { + _make_script_list_context_menu(); + } +} + +void ShaderEditorPlugin::_setup_popup_menu(PopupMenuType p_type, PopupMenu *p_menu) { + if (p_type == FILE) { + p_menu->add_shortcut(ED_SHORTCUT("shader_editor/new", TTR("New Shader..."), KeyModifierMask::CMD_OR_CTRL | Key::N), FILE_NEW); + p_menu->add_shortcut(ED_SHORTCUT("shader_editor/new_include", TTR("New Shader Include..."), KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT | Key::N), FILE_NEW_INCLUDE); + p_menu->add_separator(); + p_menu->add_shortcut(ED_SHORTCUT("shader_editor/open", TTR("Load Shader File..."), KeyModifierMask::CMD_OR_CTRL | Key::O), FILE_OPEN); + p_menu->add_shortcut(ED_SHORTCUT("shader_editor/open_include", TTR("Load Shader Include File..."), KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT | Key::O), FILE_OPEN_INCLUDE); + } + + if (p_type == FILE || p_type == CONTEXT_VALID_ITEM) { + p_menu->add_shortcut(ED_SHORTCUT("shader_editor/save", TTR("Save File"), KeyModifierMask::ALT | KeyModifierMask::CMD_OR_CTRL | Key::S), FILE_SAVE); + p_menu->add_shortcut(ED_SHORTCUT("shader_editor/save_as", TTR("Save File As...")), FILE_SAVE_AS); + } + + if (p_type == FILE) { + p_menu->add_separator(); + p_menu->add_item(TTR("Open File in Inspector"), FILE_INSPECT); + p_menu->add_separator(); + p_menu->add_shortcut(ED_SHORTCUT("shader_editor/close_file", TTR("Close File"), KeyModifierMask::CMD_OR_CTRL | Key::W), FILE_CLOSE); + } else { + p_menu->add_shortcut(ED_SHORTCUT("shader_editor/close_file", TTR("Close File"), KeyModifierMask::CMD_OR_CTRL | Key::W), FILE_CLOSE); + p_menu->add_item(TTR("Close All"), CLOSE_ALL); + p_menu->add_item(TTR("Close Other Tabs"), CLOSE_OTHER_TABS); + if (p_type == CONTEXT_VALID_ITEM) { + p_menu->add_separator(); + p_menu->add_item(TTR("Copy Script Path"), COPY_PATH); + p_menu->add_item(TTR("Show in File System"), SHOW_IN_FILE_SYSTEM); + } + } +} + +void ShaderEditorPlugin::_make_script_list_context_menu() { + context_menu->clear(); + + int selected = shader_tabs->get_current_tab(); + if (selected < 0 || selected >= shader_tabs->get_tab_count()) { + return; + } + + Control *control = shader_tabs->get_tab_control(selected); + bool is_valid_editor_control = Object::cast_to<TextShaderEditor>(control) || Object::cast_to<VisualShaderEditor>(control); + + _setup_popup_menu(is_valid_editor_control ? CONTEXT_VALID_ITEM : CONTEXT, context_menu); + + context_menu->set_item_disabled(context_menu->get_item_index(CLOSE_ALL), shader_tabs->get_tab_count() <= 0); + context_menu->set_item_disabled(context_menu->get_item_index(CLOSE_OTHER_TABS), shader_tabs->get_tab_count() <= 1); + + context_menu->set_position(main_split->get_screen_position() + main_split->get_local_mouse_position()); + context_menu->reset_size(); + context_menu->popup(); } void ShaderEditorPlugin::_close_shader(int p_index) { @@ -371,6 +438,10 @@ void ShaderEditorPlugin::_close_shader(int p_index) { edited_shaders.remove_at(p_index); _update_shader_list(); EditorUndoRedoManager::get_singleton()->clear_history(); // To prevent undo on deleted graphs. + + if (shader_tabs->get_tab_count() == 0) { + left_panel->show(); // Make sure the panel is visible, because it can't be toggled without open shaders. + } } void ShaderEditorPlugin::_close_builtin_shaders_from_scene(const String &p_scene) { @@ -486,6 +557,31 @@ void ShaderEditorPlugin::_menu_item_pressed(int p_index) { case FILE_CLOSE: { _close_shader(shader_tabs->get_current_tab()); } break; + case CLOSE_ALL: { + while (shader_tabs->get_tab_count() > 0) { + _close_shader(0); + } + } break; + case CLOSE_OTHER_TABS: { + int index = shader_tabs->get_current_tab(); + for (int i = 0; i < index; i++) { + _close_shader(0); + } + while (shader_tabs->get_tab_count() > 1) { + _close_shader(1); + } + } break; + case SHOW_IN_FILE_SYSTEM: { + Ref<Resource> shader = _get_current_shader(); + String path = shader->get_path(); + if (!path.is_empty()) { + FileSystemDock::get_singleton()->navigate_to_path(path); + } + } break; + case COPY_PATH: { + Ref<Resource> shader = _get_current_shader(); + DisplayServer::get_singleton()->clipboard_set(shader->get_path()); + } break; } } @@ -652,6 +748,14 @@ void ShaderEditorPlugin::_res_saved_callback(const Ref<Resource> &p_res) { } } +void ShaderEditorPlugin::_set_file_specific_items_disabled(bool p_disabled) { + PopupMenu *file_popup_menu = file_menu->get_popup(); + file_popup_menu->set_item_disabled(file_popup_menu->get_item_index(FILE_SAVE), p_disabled); + file_popup_menu->set_item_disabled(file_popup_menu->get_item_index(FILE_SAVE_AS), p_disabled); + file_popup_menu->set_item_disabled(file_popup_menu->get_item_index(FILE_INSPECT), p_disabled); + file_popup_menu->set_item_disabled(file_popup_menu->get_item_index(FILE_CLOSE), p_disabled); +} + void ShaderEditorPlugin::_notification(int p_what) { switch (p_what) { case NOTIFICATION_READY: { @@ -672,30 +776,22 @@ ShaderEditorPlugin::ShaderEditorPlugin() { Ref<Shortcut> make_floating_shortcut = ED_SHORTCUT_AND_COMMAND("shader_editor/make_floating", TTR("Make Floating")); window_wrapper->set_wrapped_control(main_split, make_floating_shortcut); - VBoxContainer *vb = memnew(VBoxContainer); + left_panel = memnew(VBoxContainer); HBoxContainer *menu_hb = memnew(HBoxContainer); - vb->add_child(menu_hb); + left_panel->add_child(menu_hb); file_menu = memnew(MenuButton); file_menu->set_text(TTR("File")); file_menu->set_shortcut_context(main_split); - file_menu->get_popup()->add_item(TTR("New Shader..."), FILE_NEW); - file_menu->get_popup()->add_item(TTR("New Shader Include..."), FILE_NEW_INCLUDE); - file_menu->get_popup()->add_separator(); - file_menu->get_popup()->add_item(TTR("Load Shader File..."), FILE_OPEN); - file_menu->get_popup()->add_item(TTR("Load Shader Include File..."), FILE_OPEN_INCLUDE); - file_menu->get_popup()->add_shortcut(ED_SHORTCUT("shader_editor/save", TTR("Save File"), KeyModifierMask::ALT | KeyModifierMask::CMD_OR_CTRL | Key::S), FILE_SAVE); - file_menu->get_popup()->add_shortcut(ED_SHORTCUT("shader_editor/save_as", TTR("Save File As...")), FILE_SAVE_AS); - file_menu->get_popup()->add_separator(); - file_menu->get_popup()->add_item(TTR("Open File in Inspector"), FILE_INSPECT); - file_menu->get_popup()->add_separator(); - file_menu->get_popup()->add_item(TTR("Close File"), FILE_CLOSE); + _setup_popup_menu(FILE, file_menu->get_popup()); file_menu->get_popup()->connect(SceneStringName(id_pressed), callable_mp(this, &ShaderEditorPlugin::_menu_item_pressed)); menu_hb->add_child(file_menu); - for (int i = FILE_SAVE; i < FILE_MAX; i++) { - file_menu->get_popup()->set_item_disabled(file_menu->get_popup()->get_item_index(i), true); - } + _set_file_specific_items_disabled(true); + + context_menu = memnew(PopupMenu); + add_child(context_menu); + context_menu->connect(SceneStringName(id_pressed), callable_mp(this, &ShaderEditorPlugin::_menu_item_pressed)); Control *padding = memnew(Control); padding->set_h_size_flags(Control::SIZE_EXPAND_FILL); @@ -715,13 +811,14 @@ ShaderEditorPlugin::ShaderEditorPlugin() { shader_list = memnew(ItemList); shader_list->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED); shader_list->set_v_size_flags(Control::SIZE_EXPAND_FILL); - vb->add_child(shader_list); + left_panel->add_child(shader_list); shader_list->connect(SceneStringName(item_selected), callable_mp(this, &ShaderEditorPlugin::_shader_selected)); shader_list->connect("item_clicked", callable_mp(this, &ShaderEditorPlugin::_shader_list_clicked)); + shader_list->set_allow_rmb_select(true); SET_DRAG_FORWARDING_GCD(shader_list, ShaderEditorPlugin); - main_split->add_child(vb); - vb->set_custom_minimum_size(Size2(200, 300) * EDSCALE); + main_split->add_child(left_panel); + left_panel->set_custom_minimum_size(Size2(200, 300) * EDSCALE); shader_tabs = memnew(TabContainer); shader_tabs->set_tabs_visible(false); @@ -734,7 +831,7 @@ ShaderEditorPlugin::ShaderEditorPlugin() { button = EditorNode::get_bottom_panel()->add_item(TTR("Shader Editor"), window_wrapper, ED_SHORTCUT_AND_COMMAND("bottom_panels/toggle_shader_editor_bottom_panel", TTR("Toggle Shader Editor Bottom Panel"), KeyModifierMask::ALT | Key::S)); shader_create_dialog = memnew(ShaderCreateDialog); - vb->add_child(shader_create_dialog); + main_split->add_child(shader_create_dialog); shader_create_dialog->connect("shader_created", callable_mp(this, &ShaderEditorPlugin::_shader_created)); shader_create_dialog->connect("shader_include_created", callable_mp(this, &ShaderEditorPlugin::_shader_include_created)); } diff --git a/editor/plugins/shader_editor_plugin.h b/editor/plugins/shader_editor_plugin.h index f9b9405e45..43e6af79fa 100644 --- a/editor/plugins/shader_editor_plugin.h +++ b/editor/plugins/shader_editor_plugin.h @@ -40,6 +40,7 @@ class ShaderCreateDialog; class ShaderEditor; class TabContainer; class TextShaderEditor; +class VBoxContainer; class VisualShaderEditor; class WindowWrapper; @@ -60,8 +61,6 @@ class ShaderEditorPlugin : public EditorPlugin { LocalVector<EditedShader> edited_shaders; - // Always valid operations come first in the enum, file-specific ones - // should go after FILE_SAVE which is used to build the menu accordingly. enum { FILE_NEW, FILE_NEW_INCLUDE, @@ -71,15 +70,26 @@ class ShaderEditorPlugin : public EditorPlugin { FILE_SAVE_AS, FILE_INSPECT, FILE_CLOSE, - FILE_MAX + CLOSE_ALL, + CLOSE_OTHER_TABS, + SHOW_IN_FILE_SYSTEM, + COPY_PATH, + }; + + enum PopupMenuType { + FILE, + CONTEXT, + CONTEXT_VALID_ITEM, }; HSplitContainer *main_split = nullptr; + VBoxContainer *left_panel = nullptr; ItemList *shader_list = nullptr; TabContainer *shader_tabs = nullptr; Button *button = nullptr; MenuButton *file_menu = nullptr; + PopupMenu *context_menu = nullptr; WindowWrapper *window_wrapper = nullptr; Button *make_floating = nullptr; @@ -88,15 +98,19 @@ class ShaderEditorPlugin : public EditorPlugin { float text_shader_zoom_factor = 1.0f; + Ref<Resource> _get_current_shader(); void _update_shader_list(); void _shader_selected(int p_index); void _shader_list_clicked(int p_item, Vector2 p_local_mouse_pos, MouseButton p_mouse_button_index); + void _setup_popup_menu(PopupMenuType p_type, PopupMenu *p_menu); + void _make_script_list_context_menu(); void _menu_item_pressed(int p_index); void _resource_saved(Object *obj); void _close_shader(int p_index); void _close_builtin_shaders_from_scene(const String &p_scene); void _file_removed(const String &p_removed_file); void _res_saved_callback(const Ref<Resource> &p_res); + void _set_file_specific_items_disabled(bool p_disabled); void _shader_created(Ref<Shader> p_shader); void _shader_include_created(Ref<ShaderInclude> p_shader_inc); diff --git a/editor/plugins/skeleton_3d_editor_plugin.cpp b/editor/plugins/skeleton_3d_editor_plugin.cpp index b340dd976e..dc4d4db3f8 100644 --- a/editor/plugins/skeleton_3d_editor_plugin.cpp +++ b/editor/plugins/skeleton_3d_editor_plugin.cpp @@ -47,6 +47,7 @@ #include "scene/3d/physics/physics_body_3d.h" #include "scene/gui/separator.h" #include "scene/gui/texture_rect.h" +#include "scene/property_utils.h" #include "scene/resources/3d/capsule_shape_3d.h" #include "scene/resources/skeleton_profile.h" #include "scene/resources/surface_tool.h" @@ -65,7 +66,7 @@ void BoneTransformEditor::create_editors() { // Position property. position_property = memnew(EditorPropertyVector3()); - position_property->setup(-10000, 10000, 0.001f, true); + position_property->setup(-10000, 10000, 0.001, true); position_property->set_label("Position"); position_property->set_selectable(false); position_property->connect("property_changed", callable_mp(this, &BoneTransformEditor::_value_changed)); @@ -74,7 +75,7 @@ void BoneTransformEditor::create_editors() { // Rotation property. rotation_property = memnew(EditorPropertyQuaternion()); - rotation_property->setup(-10000, 10000, 0.001f, true); + rotation_property->setup(-10000, 10000, 0.001, true); rotation_property->set_label("Rotation"); rotation_property->set_selectable(false); rotation_property->connect("property_changed", callable_mp(this, &BoneTransformEditor::_value_changed)); @@ -83,7 +84,7 @@ void BoneTransformEditor::create_editors() { // Scale property. scale_property = memnew(EditorPropertyVector3()); - scale_property->setup(-10000, 10000, 0.001f, true); + scale_property->setup(-10000, 10000, 0.001, true, true); scale_property->set_label("Scale"); scale_property->set_selectable(false); scale_property->connect("property_changed", callable_mp(this, &BoneTransformEditor::_value_changed)); @@ -97,7 +98,7 @@ void BoneTransformEditor::create_editors() { // Transform/Matrix property. rest_matrix = memnew(EditorPropertyTransform3D()); - rest_matrix->setup(-10000, 10000, 0.001f, true); + rest_matrix->setup(-10000, 10000, 0.001, true); rest_matrix->set_label("Transform"); rest_matrix->set_selectable(false); rest_section->get_vbox()->add_child(rest_matrix); @@ -122,6 +123,13 @@ void BoneTransformEditor::_value_changed(const String &p_property, const Variant undo_redo->create_action(TTR("Set Bone Transform"), UndoRedo::MERGE_ENDS); undo_redo->add_undo_property(skeleton, p_property, skeleton->get(p_property)); undo_redo->add_do_property(skeleton, p_property, p_value); + + Skeleton3DEditor *se = Skeleton3DEditor::get_singleton(); + if (se) { + undo_redo->add_do_method(se, "update_joint_tree"); + undo_redo->add_undo_method(se, "update_joint_tree"); + } + undo_redo->commit_action(); } } @@ -189,26 +197,31 @@ void BoneTransformEditor::_update_properties() { if (split[2] == "enabled") { enabled_checkbox->set_read_only(E.usage & PROPERTY_USAGE_READ_ONLY); enabled_checkbox->update_property(); + enabled_checkbox->update_editor_property_status(); enabled_checkbox->queue_redraw(); } if (split[2] == "position") { position_property->set_read_only(E.usage & PROPERTY_USAGE_READ_ONLY); position_property->update_property(); + position_property->update_editor_property_status(); position_property->queue_redraw(); } if (split[2] == "rotation") { rotation_property->set_read_only(E.usage & PROPERTY_USAGE_READ_ONLY); rotation_property->update_property(); + rotation_property->update_editor_property_status(); rotation_property->queue_redraw(); } if (split[2] == "scale") { scale_property->set_read_only(E.usage & PROPERTY_USAGE_READ_ONLY); scale_property->update_property(); + scale_property->update_editor_property_status(); scale_property->queue_redraw(); } if (split[2] == "rest") { rest_matrix->set_read_only(E.usage & PROPERTY_USAGE_READ_ONLY); rest_matrix->update_property(); + rest_matrix->update_editor_property_status(); rest_matrix->queue_redraw(); } } @@ -232,6 +245,11 @@ void Skeleton3DEditor::set_bone_options_enabled(const bool p_bone_options_enable skeleton_options->get_popup()->set_item_disabled(SKELETON_OPTION_SELECTED_POSES_TO_RESTS, !p_bone_options_enabled); }; +void Skeleton3DEditor::_bind_methods() { + ClassDB::bind_method(D_METHOD("update_all"), &Skeleton3DEditor::update_all); + ClassDB::bind_method(D_METHOD("update_joint_tree"), &Skeleton3DEditor::update_joint_tree); +} + void Skeleton3DEditor::_on_click_skeleton_option(int p_skeleton_option) { if (!skeleton) { return; @@ -294,6 +312,10 @@ void Skeleton3DEditor::reset_pose(const bool p_all_bones) { ur->add_undo_method(skeleton, "set_bone_pose_scale", selected_bone, skeleton->get_bone_pose_scale(selected_bone)); ur->add_do_method(skeleton, "reset_bone_pose", selected_bone); } + + ur->add_undo_method(this, "update_joint_tree"); + ur->add_do_method(this, "update_joint_tree"); + ur->commit_action(); } @@ -357,6 +379,10 @@ void Skeleton3DEditor::pose_to_rest(const bool p_all_bones) { ur->add_do_method(skeleton, "set_bone_rest", selected_bone, skeleton->get_bone_pose(selected_bone)); ur->add_undo_method(skeleton, "set_bone_rest", selected_bone, skeleton->get_bone_rest(selected_bone)); } + + ur->add_undo_method(this, "update_joint_tree"); + ur->add_do_method(this, "update_joint_tree"); + ur->commit_action(); } @@ -620,9 +646,12 @@ void Skeleton3DEditor::move_skeleton_bone(NodePath p_skeleton_path, int32_t p_se } ur->add_undo_method(skeleton_node, "set_bone_parent", p_selected_boneidx, skeleton_node->get_bone_parent(p_selected_boneidx)); ur->add_do_method(skeleton_node, "set_bone_parent", p_selected_boneidx, p_target_boneidx); + + ur->add_undo_method(this, "update_joint_tree"); + ur->add_do_method(this, "update_joint_tree"); + skeleton_node->set_bone_parent(p_selected_boneidx, p_target_boneidx); - update_joint_tree(); ur->commit_action(); } @@ -655,6 +684,107 @@ void Skeleton3DEditor::_joint_tree_selection_changed() { void Skeleton3DEditor::_joint_tree_rmb_select(const Vector2 &p_pos, MouseButton p_button) { } +void Skeleton3DEditor::_joint_tree_button_clicked(Object *p_item, int p_column, int p_id, MouseButton p_button) { + if (!skeleton) { + return; + } + + TreeItem *tree_item = Object::cast_to<TreeItem>(p_item); + if (tree_item) { + String tree_item_metadata = tree_item->get_metadata(0); + + String bone_enabled_property = tree_item_metadata + "/enabled"; + String bone_parent_property = tree_item_metadata + "/parent"; + String bone_name_property = tree_item_metadata + "/name"; + String bone_position_property = tree_item_metadata + "/position"; + String bone_rotation_property = tree_item_metadata + "/rotation"; + String bone_scale_property = tree_item_metadata + "/scale"; + String bone_rest_property = tree_item_metadata + "/rest"; + + Variant current_enabled = skeleton->get(bone_enabled_property); + Variant current_parent = skeleton->get(bone_parent_property); + Variant current_name = skeleton->get(bone_name_property); + Variant current_position = skeleton->get(bone_position_property); + Variant current_rotation = skeleton->get(bone_rotation_property); + Variant current_scale = skeleton->get(bone_scale_property); + Variant current_rest = skeleton->get(bone_rest_property); + + EditorUndoRedoManager *ur = EditorUndoRedoManager::get_singleton(); + ur->create_action(TTR("Revert Bone")); + + bool can_revert_enabled = EditorPropertyRevert::can_property_revert(skeleton, bone_enabled_property, ¤t_enabled); + if (can_revert_enabled) { + bool is_valid = false; + Variant new_enabled = EditorPropertyRevert::get_property_revert_value(skeleton, bone_enabled_property, &is_valid); + if (is_valid) { + ur->add_undo_method(skeleton, "set", bone_enabled_property, current_enabled); + ur->add_do_method(skeleton, "set", bone_enabled_property, new_enabled); + } + } + + bool can_revert_parent = EditorPropertyRevert::can_property_revert(skeleton, bone_parent_property, ¤t_parent); + if (can_revert_parent) { + bool is_valid = false; + Variant new_parent = EditorPropertyRevert::get_property_revert_value(skeleton, bone_parent_property, &is_valid); + if (is_valid) { + ur->add_undo_method(skeleton, "set", bone_parent_property, current_parent); + ur->add_do_method(skeleton, "set", bone_parent_property, new_parent); + } + } + bool can_revert_name = EditorPropertyRevert::can_property_revert(skeleton, bone_name_property, ¤t_name); + if (can_revert_name) { + bool is_valid = false; + Variant new_name = EditorPropertyRevert::get_property_revert_value(skeleton, bone_name_property, &is_valid); + if (is_valid) { + ur->add_undo_method(skeleton, "set", bone_name_property, current_name); + ur->add_do_method(skeleton, "set", bone_name_property, new_name); + } + } + bool can_revert_position = EditorPropertyRevert::can_property_revert(skeleton, bone_position_property, ¤t_position); + if (can_revert_position) { + bool is_valid = false; + Variant new_position = EditorPropertyRevert::get_property_revert_value(skeleton, bone_position_property, &is_valid); + if (is_valid) { + ur->add_undo_method(skeleton, "set", bone_position_property, current_position); + ur->add_do_method(skeleton, "set", bone_position_property, new_position); + } + } + bool can_revert_rotation = EditorPropertyRevert::can_property_revert(skeleton, bone_rotation_property, ¤t_rotation); + if (can_revert_rotation) { + bool is_valid = false; + Variant new_rotation = EditorPropertyRevert::get_property_revert_value(skeleton, bone_rotation_property, &is_valid); + if (is_valid) { + ur->add_undo_method(skeleton, "set", bone_rotation_property, current_rotation); + ur->add_do_method(skeleton, "set", bone_rotation_property, new_rotation); + } + } + bool can_revert_scale = EditorPropertyRevert::can_property_revert(skeleton, bone_scale_property, ¤t_scale); + if (can_revert_scale) { + bool is_valid = false; + Variant new_scale = EditorPropertyRevert::get_property_revert_value(skeleton, bone_scale_property, &is_valid); + if (is_valid) { + ur->add_undo_method(skeleton, "set", bone_scale_property, current_scale); + ur->add_do_method(skeleton, "set", bone_scale_property, new_scale); + } + } + bool can_revert_rest = EditorPropertyRevert::can_property_revert(skeleton, bone_rest_property, ¤t_rest); + if (can_revert_rest) { + bool is_valid = false; + Variant new_rest = EditorPropertyRevert::get_property_revert_value(skeleton, bone_rest_property, &is_valid); + if (is_valid) { + ur->add_undo_method(skeleton, "set", bone_rest_property, current_rest); + ur->add_do_method(skeleton, "set", bone_rest_property, new_rest); + } + } + + ur->add_undo_method(this, "update_all"); + ur->add_do_method(this, "update_all"); + + ur->commit_action(); + } + return; +} + void Skeleton3DEditor::_update_properties() { if (pose_editor) { pose_editor->_update_properties(); @@ -693,15 +823,52 @@ void Skeleton3DEditor::update_joint_tree() { joint_item->set_selectable(0, true); joint_item->set_metadata(0, "bones/" + itos(current_bone_idx)); + String bone_enabled_property = "bones/" + itos(current_bone_idx) + "/enabled"; + String bone_parent_property = "bones/" + itos(current_bone_idx) + "/parent"; + String bone_name_property = "bones/" + itos(current_bone_idx) + "/name"; + String bone_position_property = "bones/" + itos(current_bone_idx) + "/position"; + String bone_rotation_property = "bones/" + itos(current_bone_idx) + "/rotation"; + String bone_scale_property = "bones/" + itos(current_bone_idx) + "/scale"; + String bone_rest_property = "bones/" + itos(current_bone_idx) + "/rest"; + + Variant current_enabled = skeleton->get(bone_enabled_property); + Variant current_parent = skeleton->get(bone_parent_property); + Variant current_name = skeleton->get(bone_name_property); + Variant current_position = skeleton->get(bone_position_property); + Variant current_rotation = skeleton->get(bone_rotation_property); + Variant current_scale = skeleton->get(bone_scale_property); + Variant current_rest = skeleton->get(bone_rest_property); + + bool can_revert_enabled = EditorPropertyRevert::can_property_revert(skeleton, bone_enabled_property, ¤t_enabled); + bool can_revert_parent = EditorPropertyRevert::can_property_revert(skeleton, bone_parent_property, ¤t_parent); + bool can_revert_name = EditorPropertyRevert::can_property_revert(skeleton, bone_name_property, ¤t_name); + bool can_revert_position = EditorPropertyRevert::can_property_revert(skeleton, bone_position_property, ¤t_position); + bool can_revert_rotation = EditorPropertyRevert::can_property_revert(skeleton, bone_rotation_property, ¤t_rotation); + bool can_revert_scale = EditorPropertyRevert::can_property_revert(skeleton, bone_scale_property, ¤t_scale); + bool can_revert_rest = EditorPropertyRevert::can_property_revert(skeleton, bone_rest_property, ¤t_rest); + + if (can_revert_enabled || can_revert_parent || can_revert_name || can_revert_position || can_revert_rotation || can_revert_scale || can_revert_rest) { + joint_item->add_button(0, get_editor_theme_icon(SNAME("ReloadSmall")), JOINT_BUTTON_REVERT, false, TTR("Revert")); + } + // Add the bone's children to the list of bones to be processed. Vector<int> current_bone_child_bones = skeleton->get_bone_children(current_bone_idx); int child_bone_size = current_bone_child_bones.size(); for (int i = 0; i < child_bone_size; i++) { bones_to_process.push_back(current_bone_child_bones[i]); } + + if (current_bone_idx == selected_bone) { + joint_item->select(0); + } } } +void Skeleton3DEditor::update_all() { + _update_properties(); + update_joint_tree(); +} + void Skeleton3DEditor::create_editors() { set_h_size_flags(SIZE_EXPAND_FILL); set_focus_mode(FOCUS_ALL); @@ -747,7 +914,7 @@ void Skeleton3DEditor::create_editors() { edit_mode_button->set_toggle_mode(true); edit_mode_button->set_focus_mode(FOCUS_NONE); edit_mode_button->set_tooltip_text(TTR("Edit Mode\nShow buttons on joints.")); - edit_mode_button->connect("toggled", callable_mp(this, &Skeleton3DEditor::edit_mode_toggled)); + edit_mode_button->connect(SceneStringName(toggled), callable_mp(this, &Skeleton3DEditor::edit_mode_toggled)); edit_mode = false; @@ -840,6 +1007,7 @@ void Skeleton3DEditor::_notification(int p_what) { joint_tree->connect(SceneStringName(item_selected), callable_mp(this, &Skeleton3DEditor::_joint_tree_selection_changed)); joint_tree->connect("item_mouse_selected", callable_mp(this, &Skeleton3DEditor::_joint_tree_rmb_select)); + joint_tree->connect("button_clicked", callable_mp(this, &Skeleton3DEditor::_joint_tree_button_clicked)); #ifdef TOOLS_ENABLED skeleton->connect(SceneStringName(pose_updated), callable_mp(this, &Skeleton3DEditor::_draw_gizmo)); skeleton->connect(SceneStringName(pose_updated), callable_mp(this, &Skeleton3DEditor::_update_properties)); @@ -1337,6 +1505,10 @@ void Skeleton3DGizmoPlugin::commit_subgizmos(const EditorNode3DGizmo *p_gizmo, c ur->add_undo_method(skeleton, "set_bone_pose_scale", p_ids[i], se->get_bone_original_scale()); } } + + ur->add_do_method(se, "update_joint_tree"); + ur->add_undo_method(se, "update_joint_tree"); + ur->commit_action(); } diff --git a/editor/plugins/skeleton_3d_editor_plugin.h b/editor/plugins/skeleton_3d_editor_plugin.h index 79dc16ae2f..0bb58aac23 100644 --- a/editor/plugins/skeleton_3d_editor_plugin.h +++ b/editor/plugins/skeleton_3d_editor_plugin.h @@ -96,6 +96,8 @@ public: class Skeleton3DEditor : public VBoxContainer { GDCLASS(Skeleton3DEditor, VBoxContainer); + static void _bind_methods(); + friend class Skeleton3DEditorPlugin; enum SkeletonOption { @@ -116,6 +118,10 @@ class Skeleton3DEditor : public VBoxContainer { Skeleton3D *skeleton = nullptr; + enum { + JOINT_BUTTON_REVERT = 0, + }; + Tree *joint_tree = nullptr; BoneTransformEditor *rest_editor = nullptr; BoneTransformEditor *pose_editor = nullptr; @@ -149,6 +155,7 @@ class Skeleton3DEditor : public VBoxContainer { EditorFileDialog *file_export_lib = nullptr; void update_joint_tree(); + void update_all(); void create_editors(); @@ -189,6 +196,7 @@ class Skeleton3DEditor : public VBoxContainer { void _joint_tree_selection_changed(); void _joint_tree_rmb_select(const Vector2 &p_pos, MouseButton p_button); + void _joint_tree_button_clicked(Object *p_item, int p_column, int p_id, MouseButton p_button); void _update_properties(); void _subgizmo_selection_change(); diff --git a/editor/plugins/sprite_frames_editor_plugin.cpp b/editor/plugins/sprite_frames_editor_plugin.cpp index 48087e3166..37d5b787eb 100644 --- a/editor/plugins/sprite_frames_editor_plugin.cpp +++ b/editor/plugins/sprite_frames_editor_plugin.cpp @@ -504,6 +504,82 @@ void SpriteFramesEditor::_update_show_settings() { } } +void SpriteFramesEditor::_auto_slice_sprite_sheet() { + if (updating_split_settings) { + return; + } + updating_split_settings = true; + + const Size2i size = split_sheet_preview->get_texture()->get_size(); + + const Size2i split_sheet = _estimate_sprite_sheet_size(split_sheet_preview->get_texture()); + split_sheet_h->set_value(split_sheet.x); + split_sheet_v->set_value(split_sheet.y); + split_sheet_size_x->set_value(size.x / split_sheet.x); + split_sheet_size_y->set_value(size.y / split_sheet.y); + split_sheet_sep_x->set_value(0); + split_sheet_sep_y->set_value(0); + split_sheet_offset_x->set_value(0); + split_sheet_offset_y->set_value(0); + + updating_split_settings = false; + + frames_selected.clear(); + selected_count = 0; + last_frame_selected = -1; + split_sheet_preview->queue_redraw(); +} + +bool SpriteFramesEditor::_matches_background_color(const Color &p_background_color, const Color &p_pixel_color) { + if ((p_background_color.a == 0 && p_pixel_color.a == 0) || p_background_color.is_equal_approx(p_pixel_color)) { + return true; + } + + Color d = p_background_color - p_pixel_color; + // 0.04f is the threshold for how much a colour can deviate from background colour and still be considered a match. Arrived at through experimentation, can be tweaked. + return (d.r * d.r) + (d.g * d.g) + (d.b * d.b) + (d.a * d.a) < 0.04f; +} + +Size2i SpriteFramesEditor::_estimate_sprite_sheet_size(const Ref<Texture2D> p_texture) { + Ref<Image> image = p_texture->get_image(); + Size2i size = p_texture->get_size(); + + Color assumed_background_color = image->get_pixel(0, 0); + Size2i sheet_size; + + bool previous_line_background = true; + for (int x = 0; x < size.x; x++) { + int y = 0; + while (y < size.y && _matches_background_color(assumed_background_color, image->get_pixel(x, y))) { + y++; + } + bool current_line_background = (y == size.y); + if (previous_line_background && !current_line_background) { + sheet_size.x++; + } + previous_line_background = current_line_background; + } + + previous_line_background = true; + for (int y = 0; y < size.y; y++) { + int x = 0; + while (x < size.x && _matches_background_color(assumed_background_color, image->get_pixel(x, y))) { + x++; + } + bool current_line_background = (x == size.x); + if (previous_line_background && !current_line_background) { + sheet_size.y++; + } + previous_line_background = current_line_background; + } + + if (sheet_size == Size2i(0, 0) || sheet_size == Size2i(1, 1)) { + sheet_size = Size2i(4, 4); + } + + return sheet_size; +} + void SpriteFramesEditor::_prepare_sprite_sheet(const String &p_file) { Ref<Texture2D> texture = ResourceLoader::load(p_file); if (texture.is_null()) { @@ -530,10 +606,11 @@ void SpriteFramesEditor::_prepare_sprite_sheet(const String &p_file) { // Different texture, reset to 4x4. dominant_param = PARAM_FRAME_COUNT; updating_split_settings = true; - split_sheet_h->set_value(4); - split_sheet_v->set_value(4); - split_sheet_size_x->set_value(size.x / 4); - split_sheet_size_y->set_value(size.y / 4); + const Size2i split_sheet = Size2i(4, 4); + split_sheet_h->set_value(split_sheet.x); + split_sheet_v->set_value(split_sheet.y); + split_sheet_size_x->set_value(size.x / split_sheet.x); + split_sheet_size_y->set_value(size.y / split_sheet.y); split_sheet_sep_x->set_value(0); split_sheet_sep_y->set_value(0); split_sheet_offset_x->set_value(0); @@ -582,6 +659,7 @@ void SpriteFramesEditor::_notification(int p_what) { zoom_reset->set_icon(get_editor_theme_icon(SNAME("ZoomReset"))); zoom_in->set_icon(get_editor_theme_icon(SNAME("ZoomMore"))); add_anim->set_icon(get_editor_theme_icon(SNAME("New"))); + duplicate_anim->set_icon(get_editor_theme_icon(SNAME("Duplicate"))); delete_anim->set_icon(get_editor_theme_icon(SNAME("Remove"))); anim_search_box->set_right_icon(get_editor_theme_icon(SNAME("Search"))); split_sheet_zoom_out->set_icon(get_editor_theme_icon(SNAME("ZoomLess"))); @@ -1102,6 +1180,41 @@ void SpriteFramesEditor::_animation_add() { animations->grab_focus(); } +void SpriteFramesEditor::_animation_duplicate() { + if (updating) { + return; + } + + if (!frames->has_animation(edited_anim)) { + return; + } + + int counter = 1; + String new_name = edited_anim; + PackedStringArray name_component = new_name.rsplit("_", true, 1); + String base_name = name_component[0]; + if (name_component.size() > 1 && name_component[1].is_valid_int() && name_component[1].to_int() >= 0) { + counter = name_component[1].to_int(); + } + new_name = base_name + "_" + itos(counter); + while (frames->has_animation(new_name)) { + counter++; + new_name = base_name + "_" + itos(counter); + } + + EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); + undo_redo->create_action(TTR("Duplicate Animation"), UndoRedo::MERGE_DISABLE, EditorNode::get_singleton()->get_edited_scene()); + undo_redo->add_do_method(frames.ptr(), "duplicate_animation", edited_anim, new_name); + undo_redo->add_undo_method(frames.ptr(), "remove_animation", new_name); + undo_redo->add_do_method(this, "_select_animation", new_name); + undo_redo->add_undo_method(this, "_select_animation", edited_anim); + undo_redo->add_do_method(this, "_update_library"); + undo_redo->add_undo_method(this, "_update_library"); + undo_redo->commit_action(); + + animations->grab_focus(); +} + void SpriteFramesEditor::_animation_remove() { if (updating) { return; @@ -1464,6 +1577,7 @@ void SpriteFramesEditor::edit(Ref<SpriteFrames> p_frames) { _zoom_reset(); add_anim->set_disabled(read_only); + duplicate_anim->set_disabled(read_only); delete_anim->set_disabled(read_only); anim_speed->set_editable(!read_only); anim_loop->set_disabled(read_only); @@ -1788,6 +1902,11 @@ SpriteFramesEditor::SpriteFramesEditor() { hbc_animlist->add_child(add_anim); add_anim->connect(SceneStringName(pressed), callable_mp(this, &SpriteFramesEditor::_animation_add)); + duplicate_anim = memnew(Button); + duplicate_anim->set_flat(true); + hbc_animlist->add_child(duplicate_anim); + duplicate_anim->connect(SceneStringName(pressed), callable_mp(this, &SpriteFramesEditor::_animation_duplicate)); + delete_anim = memnew(Button); delete_anim->set_theme_type_variation("FlatButton"); hbc_animlist->add_child(delete_anim); @@ -1841,6 +1960,8 @@ SpriteFramesEditor::SpriteFramesEditor() { add_anim->set_shortcut_context(animations); add_anim->set_shortcut(ED_SHORTCUT("sprite_frames/new_animation", TTR("Add Animation"), KeyModifierMask::CMD_OR_CTRL | Key::N)); + duplicate_anim->set_shortcut_context(animations); + duplicate_anim->set_shortcut(ED_SHORTCUT("sprite_frames/duplicate_animation", TTR("Duplicate Animation"), KeyModifierMask::CMD_OR_CTRL | Key::D)); delete_anim->set_shortcut_context(animations); delete_anim->set_shortcut(ED_SHORTCUT("sprite_frames/delete_animation", TTR("Delete Animation"), Key::KEY_DELETE)); @@ -2290,6 +2411,11 @@ SpriteFramesEditor::SpriteFramesEditor() { split_sheet_offset_hb->add_child(split_sheet_offset_vb); split_sheet_settings_vb->add_child(split_sheet_offset_hb); + Button *auto_slice = memnew(Button); + auto_slice->set_text(TTR("Auto Slice")); + auto_slice->connect(SceneStringName(pressed), callable_mp(this, &SpriteFramesEditor::_auto_slice_sprite_sheet)); + split_sheet_settings_vb->add_child(auto_slice); + split_sheet_hb->add_child(split_sheet_settings_vb); file_split_sheet = memnew(EditorFileDialog); diff --git a/editor/plugins/sprite_frames_editor_plugin.h b/editor/plugins/sprite_frames_editor_plugin.h index 9b6aaf98fe..a85a6b2453 100644 --- a/editor/plugins/sprite_frames_editor_plugin.h +++ b/editor/plugins/sprite_frames_editor_plugin.h @@ -121,6 +121,7 @@ class SpriteFramesEditor : public HSplitContainer { Vector<int> selection; Button *add_anim = nullptr; + Button *duplicate_anim = nullptr; Button *delete_anim = nullptr; SpinBox *anim_speed = nullptr; Button *anim_loop = nullptr; @@ -210,6 +211,7 @@ class SpriteFramesEditor : public HSplitContainer { void _animation_selected(); void _animation_name_edited(); void _animation_add(); + void _animation_duplicate(); void _animation_remove(); void _animation_remove_confirmed(); void _animation_search_text_changed(const String &p_text); @@ -234,6 +236,9 @@ class SpriteFramesEditor : public HSplitContainer { void drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from); void _open_sprite_sheet(); + void _auto_slice_sprite_sheet(); + bool _matches_background_color(const Color &p_background_color, const Color &p_pixel_color); + Size2i _estimate_sprite_sheet_size(const Ref<Texture2D> p_texture); void _prepare_sprite_sheet(const String &p_file); int _sheet_preview_position_to_frame_index(const Vector2 &p_position); void _sheet_preview_draw(); diff --git a/editor/plugins/style_box_editor_plugin.cpp b/editor/plugins/style_box_editor_plugin.cpp index 6ecbff3bb4..0b53c10fab 100644 --- a/editor/plugins/style_box_editor_plugin.cpp +++ b/editor/plugins/style_box_editor_plugin.cpp @@ -113,7 +113,7 @@ StyleBoxPreview::StyleBoxPreview() { // This theme variation works better than the normal theme because there's no focus highlight. grid_preview->set_theme_type_variation("PreviewLightButton"); grid_preview->set_toggle_mode(true); - grid_preview->connect("toggled", callable_mp(this, &StyleBoxPreview::_grid_preview_toggled)); + grid_preview->connect(SceneStringName(toggled), callable_mp(this, &StyleBoxPreview::_grid_preview_toggled)); grid_preview->set_pressed(grid_preview_enabled); add_child(grid_preview); } diff --git a/editor/plugins/text_editor.cpp b/editor/plugins/text_editor.cpp index ecdc4acf47..c1bcd43b2e 100644 --- a/editor/plugins/text_editor.cpp +++ b/editor/plugins/text_editor.cpp @@ -35,6 +35,7 @@ #include "editor/editor_node.h" #include "editor/editor_settings.h" #include "scene/gui/menu_button.h" +#include "scene/gui/split_container.h" void TextEditor::add_syntax_highlighter(Ref<EditorSyntaxHighlighter> p_highlighter) { ERR_FAIL_COND(p_highlighter.is_null()); @@ -304,8 +305,8 @@ void TextEditor::tag_saved_version() { code_editor->get_text_editor()->tag_saved_version(); } -void TextEditor::goto_line(int p_line, bool p_with_error) { - code_editor->goto_line(p_line); +void TextEditor::goto_line(int p_line, int p_column) { + code_editor->goto_line(p_line, p_column); } void TextEditor::goto_line_selection(int p_line, int p_begin, int p_end) { @@ -606,6 +607,7 @@ TextEditor::TextEditor() { code_editor->set_anchors_and_offsets_preset(Control::PRESET_FULL_RECT); code_editor->set_v_size_flags(Control::SIZE_EXPAND_FILL); code_editor->show_toggle_scripts_button(); + code_editor->set_toggle_list_control(ScriptEditor::get_singleton()->get_left_list_split()); update_settings(); diff --git a/editor/plugins/text_editor.h b/editor/plugins/text_editor.h index 268e5c32b4..1acec4e959 100644 --- a/editor/plugins/text_editor.h +++ b/editor/plugins/text_editor.h @@ -129,7 +129,7 @@ public: virtual PackedInt32Array get_breakpoints() override; virtual void set_breakpoint(int p_line, bool p_enabled) override{}; virtual void clear_breakpoints() override{}; - virtual void goto_line(int p_line, bool p_with_error = false) override; + virtual void goto_line(int p_line, int p_column = 0) override; void goto_line_selection(int p_line, int p_begin, int p_end); virtual void set_executing_line(int p_line) override; virtual void clear_executing_line() override; diff --git a/editor/plugins/text_shader_editor.cpp b/editor/plugins/text_shader_editor.cpp index 6cd92bdf57..0ff7aaa3fe 100644 --- a/editor/plugins/text_shader_editor.cpp +++ b/editor/plugins/text_shader_editor.cpp @@ -779,7 +779,7 @@ void TextShaderEditor::_show_warnings_panel(bool p_show) { void TextShaderEditor::_warning_clicked(const Variant &p_line) { if (p_line.get_type() == Variant::INT) { - code_editor->get_text_editor()->set_caret_line(p_line.operator int64_t()); + code_editor->goto_line_centered(p_line.operator int64_t()); } } @@ -1256,4 +1256,5 @@ TextShaderEditor::TextShaderEditor() { add_child(disk_changed); _editor_settings_changed(); + code_editor->show_toggle_scripts_button(); // TODO: Disabled for now, because it doesn't work properly. } diff --git a/editor/plugins/texture_3d_editor_plugin.cpp b/editor/plugins/texture_3d_editor_plugin.cpp index fa90e982fe..9fce79622a 100644 --- a/editor/plugins/texture_3d_editor_plugin.cpp +++ b/editor/plugins/texture_3d_editor_plugin.cpp @@ -30,8 +30,25 @@ #include "texture_3d_editor_plugin.h" +#include "editor/editor_string_names.h" +#include "editor/themes/editor_scale.h" #include "scene/gui/label.h" +// Shader sources. + +constexpr const char *texture_3d_shader = R"( + // Texture3DEditor preview shader. + + shader_type canvas_item; + + uniform sampler3D tex; + uniform float layer; + + void fragment() { + COLOR = textureLod(tex, vec3(UV, layer), 0.0); + } +)"; + void Texture3DEditor::_texture_rect_draw() { texture_rect->draw_rect(Rect2(Point2(), texture_rect->get_size()), Color(1, 1, 1, 1)); } @@ -48,6 +65,13 @@ void Texture3DEditor::_notification(int p_what) { draw_texture_rect(checkerboard, Rect2(Point2(), size), true); } break; + + case NOTIFICATION_THEME_CHANGED: { + if (info) { + Ref<Font> metadata_label_font = get_theme_font(SNAME("expression"), EditorStringName(EditorFonts)); + info->add_theme_font_override(SceneStringName(font), metadata_label_font); + } + } break; } } @@ -55,35 +79,27 @@ void Texture3DEditor::_texture_changed() { if (!is_visible()) { return; } + + setting = true; + _update_gui(); + setting = false; + + _update_material(true); queue_redraw(); } -void Texture3DEditor::_update_material() { +void Texture3DEditor::_update_material(bool p_texture_changed) { material->set_shader_parameter("layer", (layer->get_value() + 0.5) / texture->get_depth()); - material->set_shader_parameter("tex", texture->get_rid()); - - String format = Image::get_format_name(texture->get_format()); - - String text; - text = itos(texture->get_width()) + "x" + itos(texture->get_height()) + "x" + itos(texture->get_depth()) + " " + format; - info->set_text(text); + if (p_texture_changed) { + material->set_shader_parameter("tex", texture->get_rid()); + } } void Texture3DEditor::_make_shaders() { shader.instantiate(); - shader->set_code(R"( -// Texture3DEditor preview shader. - -shader_type canvas_item; - -uniform sampler3D tex; -uniform float layer; + shader->set_code(texture_3d_shader); -void fragment() { - COLOR = textureLod(tex, vec3(UV, layer), 0.0); -} -)"); material.instantiate(); material->set_shader(shader); } @@ -113,6 +129,41 @@ void Texture3DEditor::_texture_rect_update_area() { texture_rect->set_size(Vector2(tex_width, tex_height)); } +void Texture3DEditor::_update_gui() { + if (texture.is_null()) { + return; + } + + _texture_rect_update_area(); + + layer->set_max(texture->get_depth() - 1); + + const String format = Image::get_format_name(texture->get_format()); + + if (texture->has_mipmaps()) { + const int mip_count = Image::get_image_required_mipmaps(texture->get_width(), texture->get_height(), texture->get_format()); + const int memory = Image::get_image_data_size(texture->get_width(), texture->get_height(), texture->get_format(), true) * texture->get_depth(); + + info->set_text(vformat(String::utf8("%d×%d×%d %s\n") + TTR("%s Mipmaps") + "\n" + TTR("Memory: %s"), + texture->get_width(), + texture->get_height(), + texture->get_depth(), + format, + mip_count, + String::humanize_size(memory))); + + } else { + const int memory = Image::get_image_data_size(texture->get_width(), texture->get_height(), texture->get_format(), false) * texture->get_depth(); + + info->set_text(vformat(String::utf8("%d×%d×%d %s\n") + TTR("No Mipmaps") + "\n" + TTR("Memory: %s"), + texture->get_width(), + texture->get_height(), + texture->get_depth(), + format, + String::humanize_size(memory))); + } +} + void Texture3DEditor::edit(Ref<Texture3D> p_texture) { if (!texture.is_null()) { texture->disconnect_changed(callable_mp(this, &Texture3DEditor::_texture_changed)); @@ -126,15 +177,17 @@ void Texture3DEditor::edit(Ref<Texture3D> p_texture) { } texture->connect_changed(callable_mp(this, &Texture3DEditor::_texture_changed)); - queue_redraw(); texture_rect->set_material(material); + setting = true; - layer->set_max(texture->get_depth() - 1); layer->set_value(0); layer->show(); - _update_material(); + _update_gui(); setting = false; - _texture_rect_update_area(); + + _update_material(true); + queue_redraw(); + } else { hide(); } @@ -142,36 +195,43 @@ void Texture3DEditor::edit(Ref<Texture3D> p_texture) { Texture3DEditor::Texture3DEditor() { set_texture_repeat(TextureRepeat::TEXTURE_REPEAT_ENABLED); - set_custom_minimum_size(Size2(1, 150)); + set_custom_minimum_size(Size2(1, 256.0) * EDSCALE); texture_rect = memnew(Control); texture_rect->set_mouse_filter(MOUSE_FILTER_IGNORE); - add_child(texture_rect); texture_rect->connect(SceneStringName(draw), callable_mp(this, &Texture3DEditor::_texture_rect_draw)); + add_child(texture_rect); + layer = memnew(SpinBox); layer->set_step(1); layer->set_max(100); - layer->set_h_grow_direction(GROW_DIRECTION_BEGIN); + layer->set_modulate(Color(1, 1, 1, 0.8)); - add_child(layer); + layer->set_h_grow_direction(GROW_DIRECTION_BEGIN); layer->set_anchor(SIDE_RIGHT, 1); layer->set_anchor(SIDE_LEFT, 1); layer->connect(SceneStringName(value_changed), callable_mp(this, &Texture3DEditor::_layer_changed)); + add_child(layer); + info = memnew(Label); + info->add_theme_color_override(SceneStringName(font_color), Color(1, 1, 1)); + info->add_theme_color_override("font_shadow_color", Color(0, 0, 0)); + info->add_theme_font_size_override(SceneStringName(font_size), 14 * EDSCALE); + info->add_theme_color_override("font_outline_color", Color(0, 0, 0)); + info->add_theme_constant_override("outline_size", 8 * EDSCALE); + info->set_h_grow_direction(GROW_DIRECTION_BEGIN); info->set_v_grow_direction(GROW_DIRECTION_BEGIN); - info->add_theme_color_override(SceneStringName(font_color), Color(1, 1, 1, 1)); - info->add_theme_color_override("font_shadow_color", Color(0, 0, 0, 0.5)); - info->add_theme_constant_override("shadow_outline_size", 1); - info->add_theme_constant_override("shadow_offset_x", 2); - info->add_theme_constant_override("shadow_offset_y", 2); - add_child(info); + info->set_h_size_flags(Control::SIZE_SHRINK_END); + info->set_v_size_flags(Control::SIZE_SHRINK_END); info->set_anchor(SIDE_RIGHT, 1); info->set_anchor(SIDE_LEFT, 1); info->set_anchor(SIDE_BOTTOM, 1); info->set_anchor(SIDE_TOP, 1); + + add_child(info); } Texture3DEditor::~Texture3DEditor() { @@ -180,7 +240,6 @@ Texture3DEditor::~Texture3DEditor() { } } -// bool EditorInspectorPlugin3DTexture::can_handle(Object *p_object) { return Object::cast_to<Texture3D>(p_object) != nullptr; } diff --git a/editor/plugins/texture_3d_editor_plugin.h b/editor/plugins/texture_3d_editor_plugin.h index 7a33a97a8f..0712ff423a 100644 --- a/editor/plugins/texture_3d_editor_plugin.h +++ b/editor/plugins/texture_3d_editor_plugin.h @@ -52,23 +52,27 @@ class Texture3DEditor : public Control { bool setting = false; void _make_shaders(); - void _update_material(); void _layer_changed(double) { if (!setting) { - _update_material(); + _update_material(false); } } + void _texture_changed(); void _texture_rect_update_area(); void _texture_rect_draw(); + void _update_material(bool p_texture_changed); + void _update_gui(); + protected: void _notification(int p_what); public: void edit(Ref<Texture3D> p_texture); + Texture3DEditor(); ~Texture3DEditor(); }; diff --git a/editor/plugins/texture_editor_plugin.cpp b/editor/plugins/texture_editor_plugin.cpp index e9d7aa9eb8..a3c1405553 100644 --- a/editor/plugins/texture_editor_plugin.cpp +++ b/editor/plugins/texture_editor_plugin.cpp @@ -145,12 +145,13 @@ TexturePreview::TexturePreview(Ref<Texture2D> p_texture, bool p_show_metadata) { p_texture->connect_changed(callable_mp(this, &TexturePreview::_update_metadata_label_text)); // It's okay that these colors are static since the grid color is static too. - metadata_label->add_theme_color_override(SceneStringName(font_color), Color::named("white")); - metadata_label->add_theme_color_override("font_shadow_color", Color::named("black")); + metadata_label->add_theme_color_override(SceneStringName(font_color), Color(1, 1, 1)); + metadata_label->add_theme_color_override("font_shadow_color", Color(0, 0, 0)); metadata_label->add_theme_font_size_override(SceneStringName(font_size), 14 * EDSCALE); - metadata_label->add_theme_color_override("font_outline_color", Color::named("black")); + metadata_label->add_theme_color_override("font_outline_color", Color(0, 0, 0)); metadata_label->add_theme_constant_override("outline_size", 8 * EDSCALE); + metadata_label->set_h_size_flags(Control::SIZE_SHRINK_END); metadata_label->set_v_size_flags(Control::SIZE_SHRINK_END); diff --git a/editor/plugins/texture_layered_editor_plugin.cpp b/editor/plugins/texture_layered_editor_plugin.cpp index 4ec9c91cf9..a8aa89a8c4 100644 --- a/editor/plugins/texture_layered_editor_plugin.cpp +++ b/editor/plugins/texture_layered_editor_plugin.cpp @@ -30,16 +30,64 @@ #include "texture_layered_editor_plugin.h" +#include "editor/editor_string_names.h" +#include "editor/themes/editor_scale.h" #include "scene/gui/label.h" +// Shader sources. + +constexpr const char *array_2d_shader = R"( + // TextureLayeredEditor preview shader (2D array). + + shader_type canvas_item; + + uniform sampler2DArray tex; + uniform float layer; + + void fragment() { + COLOR = textureLod(tex, vec3(UV, layer), 0.0); + } +)"; + +constexpr const char *cubemap_shader = R"( + // TextureLayeredEditor preview shader (cubemap). + + shader_type canvas_item; + + uniform samplerCube tex; + uniform vec3 normal; + uniform mat3 rot; + + void fragment() { + vec3 n = rot * normalize(vec3(normal.xy * (UV * 2.0 - 1.0), normal.z)); + COLOR = textureLod(tex, n, 0.0); + } +)"; + +constexpr const char *cubemap_array_shader = R"( + // TextureLayeredEditor preview shader (cubemap array). + + shader_type canvas_item; + uniform samplerCubeArray tex; + uniform vec3 normal; + uniform mat3 rot; + uniform float layer; + + void fragment() { + vec3 n = rot * normalize(vec3(normal.xy * (UV * 2.0 - 1.0), normal.z)); + COLOR = textureLod(tex, vec4(n, layer), 0.0); + } +)"; + void TextureLayeredEditor::gui_input(const Ref<InputEvent> &p_event) { ERR_FAIL_COND(p_event.is_null()); Ref<InputEventMouseMotion> mm = p_event; if (mm.is_valid() && (mm->get_button_mask().has_flag(MouseButtonMask::LEFT))) { y_rot += -mm->get_relative().x * 0.01; - x_rot += mm->get_relative().y * 0.01; - _update_material(); + x_rot += -mm->get_relative().y * 0.01; + + _update_material(false); } } @@ -47,6 +95,69 @@ void TextureLayeredEditor::_texture_rect_draw() { texture_rect->draw_rect(Rect2(Point2(), texture_rect->get_size()), Color(1, 1, 1, 1)); } +void TextureLayeredEditor::_update_gui() { + if (texture.is_null()) { + return; + } + + _texture_rect_update_area(); + + const String format = Image::get_format_name(texture->get_format()); + String texture_info; + + switch (texture->get_layered_type()) { + case TextureLayered::LAYERED_TYPE_2D_ARRAY: { + layer->set_max(texture->get_layers() - 1); + + texture_info = vformat(String::utf8("%d×%d (×%d) %s\n"), + texture->get_width(), + texture->get_height(), + texture->get_layers(), + format); + + } break; + case TextureLayered::LAYERED_TYPE_CUBEMAP: { + layer->hide(); + + texture_info = vformat(String::utf8("%d×%d %s\n"), + texture->get_width(), + texture->get_height(), + format); + + } break; + case TextureLayered::LAYERED_TYPE_CUBEMAP_ARRAY: { + layer->set_max(texture->get_layers() / 6 - 1); + + texture_info = vformat(String::utf8("%d×%d (×%d) %s\n"), + texture->get_width(), + texture->get_height(), + texture->get_layers() / 6, + format); + + } break; + + default: { + } + } + + if (texture->has_mipmaps()) { + const int mip_count = Image::get_image_required_mipmaps(texture->get_width(), texture->get_height(), texture->get_format()); + const int memory = Image::get_image_data_size(texture->get_width(), texture->get_height(), texture->get_format(), true) * texture->get_layers(); + + texture_info += vformat(TTR("%s Mipmaps") + "\n" + TTR("Memory: %s"), + mip_count, + String::humanize_size(memory)); + + } else { + const int memory = Image::get_image_data_size(texture->get_width(), texture->get_height(), texture->get_format(), false) * texture->get_layers(); + + texture_info += vformat(TTR("No Mipmaps") + "\n" + TTR("Memory: %s"), + String::humanize_size(memory)); + } + + info->set_text(texture_info); +} + void TextureLayeredEditor::_notification(int p_what) { switch (p_what) { case NOTIFICATION_RESIZED: { @@ -59,6 +170,13 @@ void TextureLayeredEditor::_notification(int p_what) { draw_texture_rect(checkerboard, Rect2(Point2(), size), true); } break; + + case NOTIFICATION_THEME_CHANGED: { + if (info) { + Ref<Font> metadata_label_font = get_theme_font(SNAME("expression"), EditorStringName(EditorFonts)); + info->add_theme_font_override(SceneStringName(font), metadata_label_font); + } + } break; } } @@ -66,13 +184,18 @@ void TextureLayeredEditor::_texture_changed() { if (!is_visible()) { return; } + + setting = true; + _update_gui(); + setting = false; + + _update_material(true); queue_redraw(); } -void TextureLayeredEditor::_update_material() { +void TextureLayeredEditor::_update_material(bool p_texture_changed) { materials[0]->set_shader_parameter("layer", layer->get_value()); materials[2]->set_shader_parameter("layer", layer->get_value()); - materials[texture->get_layered_type()]->set_shader_parameter("tex", texture->get_rid()); Vector3 v(1, 1, 1); v.normalize(); @@ -86,67 +209,20 @@ void TextureLayeredEditor::_update_material() { materials[2]->set_shader_parameter("normal", v); materials[2]->set_shader_parameter("rot", b); - String format = Image::get_format_name(texture->get_format()); - - String text; - if (texture->get_layered_type() == TextureLayered::LAYERED_TYPE_2D_ARRAY) { - text = itos(texture->get_width()) + "x" + itos(texture->get_height()) + " (x " + itos(texture->get_layers()) + ")" + format; - } else if (texture->get_layered_type() == TextureLayered::LAYERED_TYPE_CUBEMAP) { - text = itos(texture->get_width()) + "x" + itos(texture->get_height()) + " " + format; - } else if (texture->get_layered_type() == TextureLayered::LAYERED_TYPE_CUBEMAP_ARRAY) { - text = itos(texture->get_width()) + "x" + itos(texture->get_height()) + " (x " + itos(texture->get_layers() / 6) + ")" + format; + if (p_texture_changed) { + materials[texture->get_layered_type()]->set_shader_parameter("tex", texture->get_rid()); } - - info->set_text(text); } void TextureLayeredEditor::_make_shaders() { shaders[0].instantiate(); - shaders[0]->set_code(R"( -// TextureLayeredEditor preview shader (2D array). - -shader_type canvas_item; - -uniform sampler2DArray tex; -uniform float layer; - -void fragment() { - COLOR = textureLod(tex, vec3(UV, layer), 0.0); -} -)"); + shaders[0]->set_code(array_2d_shader); shaders[1].instantiate(); - shaders[1]->set_code(R"( -// TextureLayeredEditor preview shader (cubemap). - -shader_type canvas_item; - -uniform samplerCube tex; -uniform vec3 normal; -uniform mat3 rot; - -void fragment() { - vec3 n = rot * normalize(vec3(normal.xy * (UV * 2.0 - 1.0), normal.z)); - COLOR = textureLod(tex, n, 0.0); -} -)"); + shaders[1]->set_code(cubemap_shader); shaders[2].instantiate(); - shaders[2]->set_code(R"( -// TextureLayeredEditor preview shader (cubemap array). - -shader_type canvas_item; - -uniform samplerCubeArray tex; -uniform vec3 normal; -uniform mat3 rot; -uniform float layer; - -void fragment() { - vec3 n = rot * normalize(vec3(normal.xy * (UV * 2.0 - 1.0), normal.z)); - COLOR = textureLod(tex, vec4(n, layer), 0.0); -} -)"); + shaders[2]->set_code(cubemap_array_shader); for (int i = 0; i < 3; i++) { materials[i].instantiate(); @@ -192,25 +268,20 @@ void TextureLayeredEditor::edit(Ref<TextureLayered> p_texture) { } texture->connect_changed(callable_mp(this, &TextureLayeredEditor::_texture_changed)); - queue_redraw(); texture_rect->set_material(materials[texture->get_layered_type()]); + setting = true; - if (texture->get_layered_type() == TextureLayered::LAYERED_TYPE_2D_ARRAY) { - layer->set_max(texture->get_layers() - 1); - layer->set_value(0); - layer->show(); - } else if (texture->get_layered_type() == TextureLayered::LAYERED_TYPE_CUBEMAP_ARRAY) { - layer->set_max(texture->get_layers() / 6 - 1); - layer->set_value(0); - layer->show(); - } else { - layer->hide(); - } + layer->set_value(0); + layer->show(); + _update_gui(); + setting = false; + x_rot = 0; y_rot = 0; - _update_material(); - setting = false; - _texture_rect_update_area(); + + _update_material(true); + queue_redraw(); + } else { hide(); } @@ -218,42 +289,48 @@ void TextureLayeredEditor::edit(Ref<TextureLayered> p_texture) { TextureLayeredEditor::TextureLayeredEditor() { set_texture_repeat(TextureRepeat::TEXTURE_REPEAT_ENABLED); - set_custom_minimum_size(Size2(1, 150)); + set_custom_minimum_size(Size2(0, 256.0) * EDSCALE); texture_rect = memnew(Control); texture_rect->set_mouse_filter(MOUSE_FILTER_IGNORE); - add_child(texture_rect); texture_rect->connect(SceneStringName(draw), callable_mp(this, &TextureLayeredEditor::_texture_rect_draw)); + add_child(texture_rect); + layer = memnew(SpinBox); layer->set_step(1); layer->set_max(100); - layer->set_h_grow_direction(GROW_DIRECTION_BEGIN); + layer->set_modulate(Color(1, 1, 1, 0.8)); - add_child(layer); + layer->set_h_grow_direction(GROW_DIRECTION_BEGIN); layer->set_anchor(SIDE_RIGHT, 1); layer->set_anchor(SIDE_LEFT, 1); layer->connect(SceneStringName(value_changed), callable_mp(this, &TextureLayeredEditor::_layer_changed)); + add_child(layer); + info = memnew(Label); + info->add_theme_color_override(SceneStringName(font_color), Color(1, 1, 1)); + info->add_theme_color_override("font_shadow_color", Color(0, 0, 0)); + info->add_theme_font_size_override(SceneStringName(font_size), 14 * EDSCALE); + info->add_theme_color_override("font_outline_color", Color(0, 0, 0)); + info->add_theme_constant_override("outline_size", 8 * EDSCALE); + info->set_h_grow_direction(GROW_DIRECTION_BEGIN); info->set_v_grow_direction(GROW_DIRECTION_BEGIN); - info->add_theme_color_override(SceneStringName(font_color), Color(1, 1, 1, 1)); - info->add_theme_color_override("font_shadow_color", Color(0, 0, 0, 0.5)); - info->add_theme_constant_override("shadow_outline_size", 1); - info->add_theme_constant_override("shadow_offset_x", 2); - info->add_theme_constant_override("shadow_offset_y", 2); - add_child(info); + info->set_h_size_flags(Control::SIZE_SHRINK_END); + info->set_v_size_flags(Control::SIZE_SHRINK_END); info->set_anchor(SIDE_RIGHT, 1); info->set_anchor(SIDE_LEFT, 1); info->set_anchor(SIDE_BOTTOM, 1); info->set_anchor(SIDE_TOP, 1); + + add_child(info); } TextureLayeredEditor::~TextureLayeredEditor() { } -// bool EditorInspectorPluginLayeredTexture::can_handle(Object *p_object) { return Object::cast_to<TextureLayered>(p_object) != nullptr; } diff --git a/editor/plugins/texture_layered_editor_plugin.h b/editor/plugins/texture_layered_editor_plugin.h index 83729f922e..900ba94c6d 100644 --- a/editor/plugins/texture_layered_editor_plugin.h +++ b/editor/plugins/texture_layered_editor_plugin.h @@ -54,24 +54,28 @@ class TextureLayeredEditor : public Control { bool setting = false; void _make_shaders(); - void _update_material(); + void _update_material(bool p_texture_changed); void _layer_changed(double) { if (!setting) { - _update_material(); + _update_material(false); } } + void _texture_changed(); void _texture_rect_update_area(); void _texture_rect_draw(); + void _update_gui(); + protected: void _notification(int p_what); virtual void gui_input(const Ref<InputEvent> &p_event) override; public: void edit(Ref<TextureLayered> p_texture); + TextureLayeredEditor(); ~TextureLayeredEditor(); }; diff --git a/editor/plugins/theme_editor_plugin.cpp b/editor/plugins/theme_editor_plugin.cpp index 99635a2531..ea1756b65a 100644 --- a/editor/plugins/theme_editor_plugin.cpp +++ b/editor/plugins/theme_editor_plugin.cpp @@ -2174,8 +2174,26 @@ void ThemeTypeDialog::_add_type_filter_cbk(const String &p_value) { _update_add_type_options(p_value); } +void ThemeTypeDialog::_type_filter_input(const Ref<InputEvent> &p_ie) { + Ref<InputEventKey> k = p_ie; + if (k.is_valid() && k->is_pressed()) { + switch (k->get_keycode()) { + case Key::UP: + case Key::DOWN: + case Key::PAGEUP: + case Key::PAGEDOWN: { + add_type_options->gui_input(k); + add_type_filter->accept_event(); + } break; + default: + break; + } + } +} + void ThemeTypeDialog::_add_type_options_cbk(int p_index) { add_type_filter->set_text(add_type_options->get_item_text(p_index)); + add_type_filter->set_caret_column(add_type_filter->get_text().length()); } void ThemeTypeDialog::_add_type_dialog_entered(const String &p_value) { @@ -2245,6 +2263,7 @@ ThemeTypeDialog::ThemeTypeDialog() { add_type_vb->add_child(add_type_filter); add_type_filter->connect(SceneStringName(text_changed), callable_mp(this, &ThemeTypeDialog::_add_type_filter_cbk)); add_type_filter->connect("text_submitted", callable_mp(this, &ThemeTypeDialog::_add_type_dialog_entered)); + add_type_filter->connect(SceneStringName(gui_input), callable_mp(this, &ThemeTypeDialog::_type_filter_input)); Label *add_type_options_label = memnew(Label); add_type_options_label->set_text(TTR("Available Node-based types:")); diff --git a/editor/plugins/theme_editor_plugin.h b/editor/plugins/theme_editor_plugin.h index ba8e3a30b7..ae92365c32 100644 --- a/editor/plugins/theme_editor_plugin.h +++ b/editor/plugins/theme_editor_plugin.h @@ -303,6 +303,7 @@ class ThemeTypeDialog : public ConfirmationDialog { void _update_add_type_options(const String &p_filter = ""); void _add_type_filter_cbk(const String &p_value); + void _type_filter_input(const Ref<InputEvent> &p_ie); void _add_type_options_cbk(int p_index); void _add_type_dialog_entered(const String &p_value); void _add_type_dialog_activated(int p_index); diff --git a/editor/plugins/tiles/tile_data_editors.cpp b/editor/plugins/tiles/tile_data_editors.cpp index af52243c41..8dbf58e228 100644 --- a/editor/plugins/tiles/tile_data_editors.cpp +++ b/editor/plugins/tiles/tile_data_editors.cpp @@ -917,7 +917,7 @@ GenericTilePolygonEditor::GenericTilePolygonEditor() { button_expand->set_toggle_mode(true); button_expand->set_pressed(false); button_expand->set_tooltip_text(TTR("Expand editor")); - button_expand->connect("toggled", callable_mp(this, &GenericTilePolygonEditor::_toggle_expand)); + button_expand->connect(SceneStringName(toggled), callable_mp(this, &GenericTilePolygonEditor::_toggle_expand)); toolbar->add_child(button_expand); toolbar->add_child(memnew(VSeparator)); diff --git a/editor/plugins/tiles/tile_map_layer_editor.cpp b/editor/plugins/tiles/tile_map_layer_editor.cpp index 63a54372b5..7e6746dd3c 100644 --- a/editor/plugins/tiles/tile_map_layer_editor.cpp +++ b/editor/plugins/tiles/tile_map_layer_editor.cpp @@ -86,8 +86,7 @@ void TileMapLayerEditorTilesPlugin::_update_toolbar() { transform_toolbar->show(); tools_settings_vsep_2->show(); random_tile_toggle->show(); - scatter_label->show(); - scatter_spinbox->show(); + scatter_controls_container->set_visible(random_tile_toggle->is_pressed()); } else { tools_settings_vsep->show(); picker_button->show(); @@ -96,8 +95,7 @@ void TileMapLayerEditorTilesPlugin::_update_toolbar() { tools_settings_vsep_2->show(); bucket_contiguous_checkbox->show(); random_tile_toggle->show(); - scatter_label->show(); - scatter_spinbox->show(); + scatter_controls_container->set_visible(random_tile_toggle->is_pressed()); } } @@ -2330,7 +2328,7 @@ TileMapLayerEditorTilesPlugin::TileMapLayerEditorTilesPlugin() { random_tile_toggle->set_theme_type_variation("FlatButton"); random_tile_toggle->set_toggle_mode(true); random_tile_toggle->set_tooltip_text(TTR("Place Random Tile")); - random_tile_toggle->connect("toggled", callable_mp(this, &TileMapLayerEditorTilesPlugin::_on_random_tile_checkbox_toggled)); + random_tile_toggle->connect(SceneStringName(toggled), callable_mp(this, &TileMapLayerEditorTilesPlugin::_on_random_tile_checkbox_toggled)); tools_settings->add_child(random_tile_toggle); // Random tile scattering. @@ -4486,7 +4484,7 @@ TileMapLayerEditor::TileMapLayerEditor() { toggle_highlight_selected_layer_button->set_theme_type_variation("FlatButton"); toggle_highlight_selected_layer_button->set_toggle_mode(true); toggle_highlight_selected_layer_button->set_pressed(true); - toggle_highlight_selected_layer_button->connect("toggled", callable_mp(this, &TileMapLayerEditor::_highlight_selected_layer_button_toggled)); + toggle_highlight_selected_layer_button->connect(SceneStringName(toggled), callable_mp(this, &TileMapLayerEditor::_highlight_selected_layer_button_toggled)); toggle_highlight_selected_layer_button->set_tooltip_text(TTR("Highlight Selected TileMap Layer")); tile_map_toolbar->add_child(toggle_highlight_selected_layer_button); @@ -4497,7 +4495,7 @@ TileMapLayerEditor::TileMapLayerEditor() { toggle_grid_button->set_theme_type_variation("FlatButton"); toggle_grid_button->set_toggle_mode(true); toggle_grid_button->set_tooltip_text(TTR("Toggle grid visibility.")); - toggle_grid_button->connect("toggled", callable_mp(this, &TileMapLayerEditor::_on_grid_toggled)); + toggle_grid_button->connect(SceneStringName(toggled), callable_mp(this, &TileMapLayerEditor::_on_grid_toggled)); tile_map_toolbar->add_child(toggle_grid_button); // Advanced settings menu button. diff --git a/editor/plugins/version_control_editor_plugin.cpp b/editor/plugins/version_control_editor_plugin.cpp index 071be13692..1df84cb0a7 100644 --- a/editor/plugins/version_control_editor_plugin.cpp +++ b/editor/plugins/version_control_editor_plugin.cpp @@ -1006,7 +1006,7 @@ VersionControlEditorPlugin::VersionControlEditorPlugin() { toggle_vcs_choice = memnew(CheckButton); toggle_vcs_choice->set_h_size_flags(Control::SIZE_EXPAND_FILL); toggle_vcs_choice->set_pressed_no_signal(false); - toggle_vcs_choice->connect(SNAME("toggled"), callable_mp(this, &VersionControlEditorPlugin::_toggle_vcs_integration)); + toggle_vcs_choice->connect(SceneStringName(toggled), callable_mp(this, &VersionControlEditorPlugin::_toggle_vcs_integration)); toggle_vcs_hbc->add_child(toggle_vcs_choice); set_up_vbc->add_child(memnew(HSeparator)); diff --git a/editor/plugins/visual_shader_editor_plugin.cpp b/editor/plugins/visual_shader_editor_plugin.cpp index d50f5e51d5..3059d10c4c 100644 --- a/editor/plugins/visual_shader_editor_plugin.cpp +++ b/editor/plugins/visual_shader_editor_plugin.cpp @@ -43,6 +43,7 @@ #include "editor/filesystem_dock.h" #include "editor/inspector_dock.h" #include "editor/plugins/curve_editor_plugin.h" +#include "editor/plugins/material_editor_plugin.h" #include "editor/plugins/shader_editor_plugin.h" #include "editor/themes/editor_scale.h" #include "scene/animation/tween.h" @@ -50,12 +51,14 @@ #include "scene/gui/check_box.h" #include "scene/gui/code_edit.h" #include "scene/gui/color_picker.h" +#include "scene/gui/flow_container.h" #include "scene/gui/graph_edit.h" #include "scene/gui/menu_button.h" #include "scene/gui/option_button.h" #include "scene/gui/popup.h" #include "scene/gui/rich_text_label.h" #include "scene/gui/separator.h" +#include "scene/gui/split_container.h" #include "scene/gui/tree.h" #include "scene/gui/view_panner.h" #include "scene/main/window.h" @@ -255,7 +258,7 @@ void VisualShaderGraphPlugin::show_port_preview(VisualShader::Type p_type, int p vbox->add_child(offset); VisualShaderNodePortPreview *port_preview = memnew(VisualShaderNodePortPreview); - port_preview->setup(visual_shader, visual_shader->get_shader_type(), p_node_id, p_port_id, p_is_valid); + port_preview->setup(visual_shader, editor->preview_material, visual_shader->get_shader_type(), p_node_id, p_port_id, p_is_valid); port_preview->set_h_size_flags(Control::SIZE_SHRINK_CENTER); vbox->add_child(port_preview); link.preview_visible = true; @@ -1526,6 +1529,7 @@ void VisualShaderEditor::edit_shader(const Ref<Shader> &p_shader) { visual_shader->set_graph_offset(graph->get_scroll_offset() / EDSCALE); _set_mode(visual_shader->get_mode()); + preview_material->set_shader(visual_shader); _update_nodes(); } else { if (visual_shader.is_valid()) { @@ -1586,7 +1590,7 @@ void VisualShaderEditor::clear_custom_types() { } void VisualShaderEditor::add_custom_type(const String &p_name, const String &p_type, const Ref<Script> &p_script, const String &p_description, int p_return_icon_type, const String &p_category, bool p_highend) { - ERR_FAIL_COND(!p_name.is_valid_identifier()); + ERR_FAIL_COND(!p_name.is_valid_ascii_identifier()); ERR_FAIL_COND(p_type.is_empty() && !p_script.is_valid()); for (int i = 0; i < add_options.size(); i++) { @@ -1949,6 +1953,96 @@ bool VisualShaderEditor::_is_available(int p_mode) { return (p_mode == -1 || (p_mode & current_mode) != 0); } +bool VisualShaderEditor::_update_preview_parameter_tree() { + bool found = false; + bool use_filter = !param_filter_name.is_empty(); + + parameters->clear(); + TreeItem *root = parameters->create_item(); + + for (const KeyValue<String, PropertyInfo> &prop : parameter_props) { + String param_name = prop.value.name; + if (use_filter && !param_name.containsn(param_filter_name)) { + continue; + } + + TreeItem *item = parameters->create_item(root); + item->set_text(0, param_name); + item->set_meta("id", param_name); + + if (param_name == selected_param_id) { + parameters->set_selected(item); + found = true; + } + + if (prop.value.type == Variant::OBJECT) { + item->set_icon(0, get_editor_theme_icon(SNAME("ImageTexture"))); + } else { + item->set_icon(0, get_editor_theme_icon(Variant::get_type_name(prop.value.type))); + } + } + + return found; +} + +void VisualShaderEditor::_clear_preview_param() { + selected_param_id = ""; + current_prop = nullptr; + + if (param_vbox2->get_child_count() > 0) { + param_vbox2->remove_child(param_vbox2->get_child(0)); + } + + param_vbox->hide(); +} + +void VisualShaderEditor::_update_preview_parameter_list() { + material_editor->edit(preview_material.ptr(), env); + + List<PropertyInfo> properties; + RenderingServer::get_singleton()->get_shader_parameter_list(visual_shader->get_rid(), &properties); + + HashSet<String> params_to_remove; + for (const KeyValue<String, PropertyInfo> &E : parameter_props) { + params_to_remove.insert(E.key); + } + parameter_props.clear(); + + for (const PropertyInfo &prop : properties) { + String param_name = prop.name; + + if (visual_shader->_has_preview_shader_parameter(param_name)) { + preview_material->set_shader_parameter(param_name, visual_shader->_get_preview_shader_parameter(param_name)); + } else { + preview_material->set_shader_parameter(param_name, RenderingServer::get_singleton()->shader_get_parameter_default(visual_shader->get_rid(), param_name)); + } + + parameter_props.insert(param_name, prop); + params_to_remove.erase(param_name); + + if (param_name == selected_param_id) { + current_prop->update_property(); + current_prop->update_editor_property_status(); + current_prop->update_cache(); + } + } + + _update_preview_parameter_tree(); + + // Removes invalid parameters. + for (const String ¶m_name : params_to_remove) { + preview_material->set_shader_parameter(param_name, Variant()); + + if (visual_shader->_has_preview_shader_parameter(param_name)) { + visual_shader->_set_preview_shader_parameter(param_name, Variant()); + } + + if (param_name == selected_param_id) { + _clear_preview_param(); + } + } +} + void VisualShaderEditor::_update_nodes() { clear_custom_types(); Dictionary added; @@ -3573,6 +3667,7 @@ void VisualShaderEditor::_add_node(int p_idx, const Vector<Variant> &p_ops, cons bool is_curve = (Object::cast_to<VisualShaderNodeCurveTexture>(vsnode.ptr()) != nullptr); bool is_curve_xyz = (Object::cast_to<VisualShaderNodeCurveXYZTexture>(vsnode.ptr()) != nullptr); bool is_parameter = (Object::cast_to<VisualShaderNodeParameter>(vsnode.ptr()) != nullptr); + bool is_mesh_emitter = (Object::cast_to<VisualShaderNodeParticleMeshEmitter>(vsnode.ptr()) != nullptr); Point2 position = graph->get_scroll_offset(); @@ -3785,6 +3880,12 @@ void VisualShaderEditor::_add_node(int p_idx, const Vector<Variant> &p_ops, cons if (is_texture2d_array) { undo_redo->force_fixed_history(); undo_redo->add_do_method(vsnode.ptr(), "set_texture_array", ResourceLoader::load(p_resource_path)); + return; + } + + if (is_mesh_emitter) { + undo_redo->add_do_method(vsnode.ptr(), "set_mesh", ResourceLoader::load(p_resource_path)); + return; } } } @@ -4872,6 +4973,74 @@ void VisualShaderEditor::_sbox_input(const Ref<InputEvent> &p_ie) { } } +void VisualShaderEditor::_param_filter_changed(const String &p_text) { + param_filter_name = p_text; + + if (!_update_preview_parameter_tree()) { + _clear_preview_param(); + } +} + +void VisualShaderEditor::_param_property_changed(const String &p_property, const Variant &p_value, const String &p_field, bool p_changing) { + if (p_changing) { + return; + } + String raw_prop_name = p_property.trim_prefix("shader_parameter/"); + + EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); + + undo_redo->create_action(vformat(TTR("Edit Preview Parameter: %s"), p_property)); + undo_redo->add_do_method(visual_shader.ptr(), "_set_preview_shader_parameter", raw_prop_name, p_value); + undo_redo->add_undo_method(visual_shader.ptr(), "_set_preview_shader_parameter", raw_prop_name, preview_material->get(p_property)); + undo_redo->add_do_method(this, "_update_current_param"); + undo_redo->add_undo_method(this, "_update_current_param"); + undo_redo->commit_action(); +} + +void VisualShaderEditor::_update_current_param() { + if (current_prop != nullptr) { + String name = current_prop->get_meta("id"); + preview_material->set("shader_parameter/" + name, visual_shader->_get_preview_shader_parameter(name)); + + current_prop->update_property(); + current_prop->update_editor_property_status(); + current_prop->update_cache(); + } +} + +void VisualShaderEditor::_param_selected() { + _clear_preview_param(); + + TreeItem *item = parameters->get_selected(); + selected_param_id = item->get_meta("id"); + + PropertyInfo pi = parameter_props.get(selected_param_id); + EditorProperty *prop = EditorInspector::instantiate_property_editor(preview_material.ptr(), pi.type, pi.name, pi.hint, pi.hint_string, pi.usage); + if (!prop) { + return; + } + prop->connect("property_changed", callable_mp(this, &VisualShaderEditor::_param_property_changed)); + prop->set_h_size_flags(SIZE_EXPAND_FILL); + prop->set_object_and_property(preview_material.ptr(), "shader_parameter/" + pi.name); + + prop->set_label(TTR("Value:")); + prop->update_property(); + prop->update_editor_property_status(); + prop->update_cache(); + + current_prop = prop; + current_prop->set_meta("id", selected_param_id); + + param_vbox2->add_child(prop); + param_vbox->show(); +} + +void VisualShaderEditor::_param_unselected() { + parameters->deselect_all(); + + _clear_preview_param(); +} + void VisualShaderEditor::_notification(int p_what) { switch (p_what) { case NOTIFICATION_POSTINITIALIZE: { @@ -4915,9 +5084,11 @@ void VisualShaderEditor::_notification(int p_what) { case NOTIFICATION_THEME_CHANGED: { highend_label->set_modulate(get_theme_color(SNAME("highend_color"), EditorStringName(Editor))); + param_filter->set_right_icon(Control::get_editor_theme_icon(SNAME("Search"))); node_filter->set_right_icon(Control::get_editor_theme_icon(SNAME("Search"))); - preview_shader->set_icon(Control::get_editor_theme_icon(SNAME("Shader"))); + code_preview_button->set_icon(Control::get_editor_theme_icon(SNAME("Shader"))); + shader_preview_button->set_icon(Control::get_editor_theme_icon(SNAME("SubViewport"))); { Color background_color = EDITOR_GET("text_editor/theme/highlighting/background_color"); @@ -4970,7 +5141,7 @@ void VisualShaderEditor::_notification(int p_what) { tools->set_icon(get_editor_theme_icon(SNAME("Tools"))); - if (p_what == NOTIFICATION_THEME_CHANGED && is_visible_in_tree()) { + if (is_visible_in_tree()) { _update_graph(); } } break; @@ -5627,7 +5798,7 @@ void VisualShaderEditor::_varying_create() { } void VisualShaderEditor::_varying_name_changed(const String &p_name) { - if (!p_name.is_valid_identifier()) { + if (!p_name.is_valid_ascii_identifier()) { varying_error_label->show(); varying_error_label->set_text(TTR("Invalid name for varying.")); add_varying_dialog->get_ok_button()->set_disabled(true); @@ -5898,6 +6069,11 @@ void VisualShaderEditor::drop_data_fw(const Point2 &p_point, const Variant &p_da saved_node_pos = p_point + Vector2(0, i * 250 * EDSCALE); saved_node_pos_dirty = true; _add_node(cubemap_node_option_idx, {}, arr[i], i); + } else if (type == "Mesh" && visual_shader->get_mode() == Shader::MODE_PARTICLES && + (visual_shader->get_shader_type() == VisualShader::TYPE_START || visual_shader->get_shader_type() == VisualShader::TYPE_START_CUSTOM)) { + saved_node_pos = p_point + Vector2(0, i * 250 * EDSCALE); + saved_node_pos_dirty = true; + _add_node(mesh_emitter_option_idx, {}, arr[i], i); } } } @@ -5907,14 +6083,14 @@ void VisualShaderEditor::drop_data_fw(const Point2 &p_point, const Variant &p_da } void VisualShaderEditor::_show_preview_text() { - preview_showed = !preview_showed; - if (preview_showed) { - if (preview_first) { - preview_window->set_size(Size2(400 * EDSCALE, 600 * EDSCALE)); - preview_window->popup_centered(); - preview_first = false; + code_preview_showed = !code_preview_showed; + if (code_preview_showed) { + if (code_preview_first) { + code_preview_window->set_size(Size2(400 * EDSCALE, 600 * EDSCALE)); + code_preview_window->popup_centered(); + code_preview_first = false; } else { - preview_window->popup(); + code_preview_window->popup(); } _preview_size_changed(); @@ -5923,18 +6099,18 @@ void VisualShaderEditor::_show_preview_text() { pending_update_preview = false; } } else { - preview_window->hide(); + code_preview_window->hide(); } } void VisualShaderEditor::_preview_close_requested() { - preview_showed = false; - preview_window->hide(); - preview_shader->set_pressed(false); + code_preview_showed = false; + code_preview_window->hide(); + code_preview_button->set_pressed(false); } void VisualShaderEditor::_preview_size_changed() { - preview_vbox->set_custom_minimum_size(preview_window->get_size()); + code_preview_vbox->set_custom_minimum_size(code_preview_window->get_size()); } static ShaderLanguage::DataType _visual_shader_editor_get_global_shader_uniform_type(const StringName &p_variable) { @@ -5943,7 +6119,7 @@ static ShaderLanguage::DataType _visual_shader_editor_get_global_shader_uniform_ } void VisualShaderEditor::_update_preview() { - if (!preview_showed) { + if (!code_preview_showed) { pending_update_preview = true; return; } @@ -6035,14 +6211,25 @@ void VisualShaderEditor::_get_next_nodes_recursively(VisualShader::Type p_type, void VisualShaderEditor::_visibility_changed() { if (!is_visible()) { - if (preview_window->is_visible()) { - preview_shader->set_pressed(false); - preview_window->hide(); - preview_showed = false; + if (code_preview_window->is_visible()) { + code_preview_button->set_pressed(false); + code_preview_window->hide(); + code_preview_showed = false; } } } +void VisualShaderEditor::_show_shader_preview() { + shader_preview_showed = !shader_preview_showed; + if (shader_preview_showed) { + shader_preview_vbox->show(); + } else { + shader_preview_vbox->hide(); + + _param_unselected(); + } +} + void VisualShaderEditor::_bind_methods() { ClassDB::bind_method("_update_nodes", &VisualShaderEditor::_update_nodes); ClassDB::bind_method("_update_graph", &VisualShaderEditor::_update_graph); @@ -6057,6 +6244,7 @@ void VisualShaderEditor::_bind_methods() { ClassDB::bind_method("_update_constant", &VisualShaderEditor::_update_constant); ClassDB::bind_method("_update_parameter", &VisualShaderEditor::_update_parameter); ClassDB::bind_method("_update_next_previews", &VisualShaderEditor::_update_next_previews); + ClassDB::bind_method("_update_current_param", &VisualShaderEditor::_update_current_param); } VisualShaderEditor::VisualShaderEditor() { @@ -6065,14 +6253,19 @@ VisualShaderEditor::VisualShaderEditor() { FileSystemDock::get_singleton()->get_script_create_dialog()->connect("script_created", callable_mp(this, &VisualShaderEditor::_script_created)); FileSystemDock::get_singleton()->connect("resource_removed", callable_mp(this, &VisualShaderEditor::_resource_removed)); + HSplitContainer *main_box = memnew(HSplitContainer); + main_box->set_anchors_and_offsets_preset(Control::PRESET_FULL_RECT); + add_child(main_box); + graph = memnew(GraphEdit); - graph->get_menu_hbox()->set_h_size_flags(SIZE_EXPAND_FILL); - graph->set_anchors_and_offsets_preset(Control::PRESET_FULL_RECT); + graph->set_v_size_flags(SIZE_EXPAND_FILL); + graph->set_h_size_flags(SIZE_EXPAND_FILL); + graph->set_custom_minimum_size(Size2(200 * EDSCALE, 0)); graph->set_grid_pattern(GraphEdit::GridPattern::GRID_PATTERN_DOTS); int grid_pattern = EDITOR_GET("editors/visual_editors/grid_pattern"); graph->set_grid_pattern((GraphEdit::GridPattern)grid_pattern); graph->set_show_zoom_label(true); - add_child(graph); + main_box->add_child(graph); SET_DRAG_FORWARDING_GCD(graph, VisualShaderEditor); float graph_minimap_opacity = EDITOR_GET("editors/visual_editors/minimap_opacity"); graph->set_minimap_opacity(graph_minimap_opacity); @@ -6160,15 +6353,36 @@ VisualShaderEditor::VisualShaderEditor() { graph->add_valid_connection_type(VisualShaderNode::PORT_TYPE_TRANSFORM, VisualShaderNode::PORT_TYPE_TRANSFORM); graph->add_valid_connection_type(VisualShaderNode::PORT_TYPE_SAMPLER, VisualShaderNode::PORT_TYPE_SAMPLER); + PanelContainer *toolbar_panel = static_cast<PanelContainer *>(graph->get_menu_hbox()->get_parent()); + toolbar_panel->set_anchors_and_offsets_preset(Control::PRESET_TOP_WIDE, PRESET_MODE_MINSIZE, 10); + toolbar_panel->set_mouse_filter(Control::MOUSE_FILTER_IGNORE); + + HFlowContainer *toolbar = memnew(HFlowContainer); + { + LocalVector<Node *> nodes; + for (int i = 0; i < graph->get_menu_hbox()->get_child_count(); i++) { + Node *child = graph->get_menu_hbox()->get_child(i); + nodes.push_back(child); + } + + for (Node *node : nodes) { + graph->get_menu_hbox()->remove_child(node); + toolbar->add_child(node); + } + + graph->get_menu_hbox()->hide(); + toolbar_panel->add_child(toolbar); + } + VSeparator *vs = memnew(VSeparator); - graph->get_menu_hbox()->add_child(vs); - graph->get_menu_hbox()->move_child(vs, 0); + toolbar->add_child(vs); + toolbar->move_child(vs, 0); custom_mode_box = memnew(CheckBox); custom_mode_box->set_text(TTR("Custom")); custom_mode_box->set_pressed(false); custom_mode_box->set_visible(false); - custom_mode_box->connect("toggled", callable_mp(this, &VisualShaderEditor::_custom_mode_toggled)); + custom_mode_box->connect(SceneStringName(toggled), callable_mp(this, &VisualShaderEditor::_custom_mode_toggled)); edit_type_standard = memnew(OptionButton); edit_type_standard->add_item(TTR("Vertex")); @@ -6196,22 +6410,22 @@ VisualShaderEditor::VisualShaderEditor() { edit_type = edit_type_standard; - graph->get_menu_hbox()->add_child(custom_mode_box); - graph->get_menu_hbox()->move_child(custom_mode_box, 0); - graph->get_menu_hbox()->add_child(edit_type_standard); - graph->get_menu_hbox()->move_child(edit_type_standard, 0); - graph->get_menu_hbox()->add_child(edit_type_particles); - graph->get_menu_hbox()->move_child(edit_type_particles, 0); - graph->get_menu_hbox()->add_child(edit_type_sky); - graph->get_menu_hbox()->move_child(edit_type_sky, 0); - graph->get_menu_hbox()->add_child(edit_type_fog); - graph->get_menu_hbox()->move_child(edit_type_fog, 0); + toolbar->add_child(custom_mode_box); + toolbar->move_child(custom_mode_box, 0); + toolbar->add_child(edit_type_standard); + toolbar->move_child(edit_type_standard, 0); + toolbar->add_child(edit_type_particles); + toolbar->move_child(edit_type_particles, 0); + toolbar->add_child(edit_type_sky); + toolbar->move_child(edit_type_sky, 0); + toolbar->add_child(edit_type_fog); + toolbar->move_child(edit_type_fog, 0); add_node = memnew(Button); add_node->set_flat(true); add_node->set_text(TTR("Add Node...")); - graph->get_menu_hbox()->add_child(add_node); - graph->get_menu_hbox()->move_child(add_node, 0); + toolbar->add_child(add_node); + toolbar->move_child(add_node, 0); add_node->connect(SceneStringName(pressed), callable_mp(this, &VisualShaderEditor::_show_members_dialog).bind(false, VisualShaderNode::PORT_TYPE_MAX, VisualShaderNode::PORT_TYPE_MAX)); graph->connect("graph_elements_linked_to_frame_request", callable_mp(this, &VisualShaderEditor::_nodes_linked_to_frame_request)); @@ -6220,46 +6434,54 @@ VisualShaderEditor::VisualShaderEditor() { varying_button = memnew(MenuButton); varying_button->set_text(TTR("Manage Varyings")); varying_button->set_switch_on_hover(true); - graph->get_menu_hbox()->add_child(varying_button); + toolbar->add_child(varying_button); PopupMenu *varying_menu = varying_button->get_popup(); varying_menu->add_item(TTR("Add Varying"), int(VaryingMenuOptions::ADD)); varying_menu->add_item(TTR("Remove Varying"), int(VaryingMenuOptions::REMOVE)); varying_menu->connect(SceneStringName(id_pressed), callable_mp(this, &VisualShaderEditor::_varying_menu_id_pressed)); - preview_shader = memnew(Button); - preview_shader->set_theme_type_variation("FlatButton"); - preview_shader->set_toggle_mode(true); - preview_shader->set_tooltip_text(TTR("Show generated shader code.")); - graph->get_menu_hbox()->add_child(preview_shader); - preview_shader->connect(SceneStringName(pressed), callable_mp(this, &VisualShaderEditor::_show_preview_text)); + code_preview_button = memnew(Button); + code_preview_button->set_theme_type_variation("FlatButton"); + code_preview_button->set_toggle_mode(true); + code_preview_button->set_tooltip_text(TTR("Show generated shader code.")); + toolbar->add_child(code_preview_button); + code_preview_button->connect(SceneStringName(pressed), callable_mp(this, &VisualShaderEditor::_show_preview_text)); + + shader_preview_button = memnew(Button); + shader_preview_button->set_theme_type_variation("FlatButton"); + shader_preview_button->set_toggle_mode(true); + shader_preview_button->set_tooltip_text(TTR("Toggle shader preview.")); + shader_preview_button->set_pressed(true); + toolbar->add_child(shader_preview_button); + shader_preview_button->connect(SceneStringName(pressed), callable_mp(this, &VisualShaderEditor::_show_shader_preview)); /////////////////////////////////////// - // PREVIEW WINDOW + // CODE PREVIEW /////////////////////////////////////// - preview_window = memnew(Window); - preview_window->set_title(TTR("Generated Shader Code")); - preview_window->set_visible(preview_showed); - preview_window->set_exclusive(true); - preview_window->connect("close_requested", callable_mp(this, &VisualShaderEditor::_preview_close_requested)); - preview_window->connect("size_changed", callable_mp(this, &VisualShaderEditor::_preview_size_changed)); - add_child(preview_window); + code_preview_window = memnew(Window); + code_preview_window->set_title(TTR("Generated Shader Code")); + code_preview_window->set_visible(code_preview_showed); + code_preview_window->set_exclusive(true); + code_preview_window->connect("close_requested", callable_mp(this, &VisualShaderEditor::_preview_close_requested)); + code_preview_window->connect("size_changed", callable_mp(this, &VisualShaderEditor::_preview_size_changed)); + add_child(code_preview_window); - preview_vbox = memnew(VBoxContainer); - preview_window->add_child(preview_vbox); - preview_vbox->add_theme_constant_override("separation", 0); + code_preview_vbox = memnew(VBoxContainer); + code_preview_window->add_child(code_preview_vbox); + code_preview_vbox->add_theme_constant_override("separation", 0); preview_text = memnew(CodeEdit); syntax_highlighter.instantiate(); - preview_vbox->add_child(preview_text); + code_preview_vbox->add_child(preview_text); preview_text->set_v_size_flags(Control::SIZE_EXPAND_FILL); preview_text->set_syntax_highlighter(syntax_highlighter); preview_text->set_draw_line_numbers(true); preview_text->set_editable(false); error_panel = memnew(PanelContainer); - preview_vbox->add_child(error_panel); + code_preview_vbox->add_child(error_panel); error_panel->set_visible(false); error_label = memnew(Label); @@ -6291,6 +6513,70 @@ VisualShaderEditor::VisualShaderEditor() { connection_popup_menu->connect(SceneStringName(id_pressed), callable_mp(this, &VisualShaderEditor::_connection_menu_id_pressed)); /////////////////////////////////////// + // SHADER PREVIEW + /////////////////////////////////////// + + shader_preview_vbox = memnew(VBoxContainer); + shader_preview_vbox->set_custom_minimum_size(Size2(200 * EDSCALE, 0)); + main_box->add_child(shader_preview_vbox); + + VSplitContainer *preview_split = memnew(VSplitContainer); + preview_split->set_v_size_flags(SIZE_EXPAND_FILL); + shader_preview_vbox->add_child(preview_split); + + // Initialize material editor. + { + env.instantiate(); + Ref<Sky> sky = memnew(Sky()); + env->set_sky(sky); + env->set_background(Environment::BG_COLOR); + env->set_ambient_source(Environment::AMBIENT_SOURCE_SKY); + env->set_reflection_source(Environment::REFLECTION_SOURCE_SKY); + + preview_material.instantiate(); + preview_material->connect(CoreStringName(property_list_changed), callable_mp(this, &VisualShaderEditor::_update_preview_parameter_list)); + + material_editor = memnew(MaterialEditor); + preview_split->add_child(material_editor); + } + + VBoxContainer *params_vbox = memnew(VBoxContainer); + preview_split->add_child(params_vbox); + + param_filter = memnew(LineEdit); + param_filter->connect(SceneStringName(text_changed), callable_mp(this, &VisualShaderEditor::_param_filter_changed)); + param_filter->set_h_size_flags(SIZE_EXPAND_FILL); + param_filter->set_placeholder(TTR("Filter Parameters")); + params_vbox->add_child(param_filter); + + ScrollContainer *sc = memnew(ScrollContainer); + sc->set_v_size_flags(SIZE_EXPAND_FILL); + params_vbox->add_child(sc); + + parameters = memnew(Tree); + parameters->set_hide_root(true); + parameters->set_allow_reselect(true); + parameters->set_hide_folding(false); + parameters->set_h_size_flags(SIZE_EXPAND_FILL); + parameters->set_v_size_flags(SIZE_EXPAND_FILL); + parameters->connect(SceneStringName(item_selected), callable_mp(this, &VisualShaderEditor::_param_selected)); + parameters->connect("nothing_selected", callable_mp(this, &VisualShaderEditor::_param_unselected)); + sc->add_child(parameters); + + param_vbox = memnew(VBoxContainer); + param_vbox->set_v_size_flags(SIZE_EXPAND_FILL); + param_vbox->hide(); + params_vbox->add_child(param_vbox); + + ScrollContainer *sc2 = memnew(ScrollContainer); + sc2->set_v_size_flags(SIZE_EXPAND_FILL); + param_vbox->add_child(sc2); + + param_vbox2 = memnew(VBoxContainer); + param_vbox2->set_h_size_flags(SIZE_EXPAND_FILL); + sc2->add_child(param_vbox2); + + /////////////////////////////////////// // SHADER NODES TREE /////////////////////////////////////// @@ -6533,6 +6819,7 @@ VisualShaderEditor::VisualShaderEditor() { // NODE3D-FOR-ALL + add_options.push_back(AddOption("ClipSpaceFar", "Input/All", "VisualShaderNodeInput", vformat(input_param_shader_modes, "clip_space_far", "CLIP_SPACE_FAR"), { "clip_space_far" }, VisualShaderNode::PORT_TYPE_SCALAR, -1, Shader::MODE_SPATIAL)); add_options.push_back(AddOption("Exposure", "Input/All", "VisualShaderNodeInput", vformat(input_param_shader_modes, "exposure", "EXPOSURE"), { "exposure" }, VisualShaderNode::PORT_TYPE_SCALAR, -1, Shader::MODE_SPATIAL)); add_options.push_back(AddOption("InvProjectionMatrix", "Input/All", "VisualShaderNodeInput", vformat(input_param_shader_modes, "inv_projection_matrix", "INV_PROJECTION_MATRIX"), { "inv_projection_matrix" }, VisualShaderNode::PORT_TYPE_TRANSFORM, -1, Shader::MODE_SPATIAL)); add_options.push_back(AddOption("InvViewMatrix", "Input/All", "VisualShaderNodeInput", vformat(input_param_shader_modes, "inv_view_matrix", "INV_VIEW_MATRIX"), { "inv_view_matrix" }, VisualShaderNode::PORT_TYPE_TRANSFORM, -1, Shader::MODE_SPATIAL)); @@ -6591,14 +6878,20 @@ VisualShaderEditor::VisualShaderEditor() { // NODE3D INPUTS add_options.push_back(AddOption("Binormal", "Input/Vertex", "VisualShaderNodeInput", vformat(input_param_for_vertex_and_fragment_shader_modes, "binormal", "BINORMAL"), { "binormal" }, VisualShaderNode::PORT_TYPE_VECTOR_3D, TYPE_FLAGS_VERTEX, Shader::MODE_SPATIAL)); + add_options.push_back(AddOption("CameraDirectionWorld", "Input/Vertex", "VisualShaderNodeInput", vformat(input_param_for_vertex_and_fragment_shader_modes, "camera_direction_world", "CAMERA_DIRECTION_WORLD"), { "camera_direction_world" }, VisualShaderNode::PORT_TYPE_VECTOR_3D, TYPE_FLAGS_VERTEX, Shader::MODE_SPATIAL)); + add_options.push_back(AddOption("CameraPositionWorld", "Input/Vertex", "VisualShaderNodeInput", vformat(input_param_for_vertex_and_fragment_shader_modes, "camera_position_world", "CAMERA_POSITION_WORLD"), { "camera_position_world" }, VisualShaderNode::PORT_TYPE_VECTOR_3D, TYPE_FLAGS_VERTEX, Shader::MODE_SPATIAL)); + add_options.push_back(AddOption("CameraVisibleLayers", "Input/Vertex", "VisualShaderNodeInput", vformat(input_param_for_vertex_and_fragment_shader_modes, "camera_visible_layers", "CAMERA_VISIBLE_LAYERS"), { "camera_visible_layers" }, VisualShaderNode::PORT_TYPE_SCALAR_UINT, TYPE_FLAGS_VERTEX, Shader::MODE_SPATIAL)); add_options.push_back(AddOption("Color", "Input/Vertex", "VisualShaderNodeInput", vformat(input_param_for_vertex_and_fragment_shader_modes, "color", "COLOR"), { "color" }, VisualShaderNode::PORT_TYPE_VECTOR_4D, TYPE_FLAGS_VERTEX, Shader::MODE_SPATIAL)); add_options.push_back(AddOption("Custom0", "Input/Vertex", "VisualShaderNodeInput", vformat(input_param_for_vertex_shader_mode, "custom0", "CUSTOM0"), { "custom0" }, VisualShaderNode::PORT_TYPE_VECTOR_4D, TYPE_FLAGS_VERTEX, Shader::MODE_SPATIAL)); add_options.push_back(AddOption("Custom1", "Input/Vertex", "VisualShaderNodeInput", vformat(input_param_for_vertex_shader_mode, "custom1", "CUSTOM1"), { "custom1" }, VisualShaderNode::PORT_TYPE_VECTOR_4D, TYPE_FLAGS_VERTEX, Shader::MODE_SPATIAL)); add_options.push_back(AddOption("Custom2", "Input/Vertex", "VisualShaderNodeInput", vformat(input_param_for_vertex_shader_mode, "custom2", "CUSTOM2"), { "custom2" }, VisualShaderNode::PORT_TYPE_VECTOR_4D, TYPE_FLAGS_VERTEX, Shader::MODE_SPATIAL)); add_options.push_back(AddOption("Custom3", "Input/Vertex", "VisualShaderNodeInput", vformat(input_param_for_vertex_shader_mode, "custom3", "CUSTOM3"), { "custom3" }, VisualShaderNode::PORT_TYPE_VECTOR_4D, TYPE_FLAGS_VERTEX, Shader::MODE_SPATIAL)); - add_options.push_back(AddOption("InstanceId", "Input/Vertex", "VisualShaderNodeInput", vformat(input_param_for_vertex_shader_mode, "instance_id", "INSTANCE_ID"), { "instance_id" }, VisualShaderNode::PORT_TYPE_SCALAR_INT, TYPE_FLAGS_VERTEX, Shader::MODE_SPATIAL)); + add_options.push_back(AddOption("EyeOffset", "Input/Vertex", "VisualShaderNodeInput", vformat(input_param_for_vertex_and_fragment_shader_modes, "eye_offset", "EYE_OFFSET"), { "eye_offset" }, VisualShaderNode::PORT_TYPE_VECTOR_3D, TYPE_FLAGS_VERTEX, Shader::MODE_SPATIAL)); add_options.push_back(AddOption("InstanceCustom", "Input/Vertex", "VisualShaderNodeInput", vformat(input_param_for_vertex_shader_mode, "instance_custom", "INSTANCE_CUSTOM"), { "instance_custom" }, VisualShaderNode::PORT_TYPE_VECTOR_4D, TYPE_FLAGS_VERTEX, Shader::MODE_SPATIAL)); + add_options.push_back(AddOption("InstanceId", "Input/Vertex", "VisualShaderNodeInput", vformat(input_param_for_vertex_shader_mode, "instance_id", "INSTANCE_ID"), { "instance_id" }, VisualShaderNode::PORT_TYPE_SCALAR_INT, TYPE_FLAGS_VERTEX, Shader::MODE_SPATIAL)); add_options.push_back(AddOption("ModelViewMatrix", "Input/Vertex", "VisualShaderNodeInput", vformat(input_param_for_vertex_shader_mode, "modelview_matrix", "MODELVIEW_MATRIX"), { "modelview_matrix" }, VisualShaderNode::PORT_TYPE_TRANSFORM, TYPE_FLAGS_VERTEX, Shader::MODE_SPATIAL)); + add_options.push_back(AddOption("NodePositionView", "Input/Vertex", "VisualShaderNodeInput", vformat(input_param_for_vertex_and_fragment_shader_modes, "node_position_view", "NODE_POSITION_VIEW"), { "node_position_view" }, VisualShaderNode::PORT_TYPE_VECTOR_3D, TYPE_FLAGS_VERTEX, Shader::MODE_SPATIAL)); + add_options.push_back(AddOption("NodePositionWorld", "Input/Vertex", "VisualShaderNodeInput", vformat(input_param_for_vertex_and_fragment_shader_modes, "node_position_world", "NODE_POSITION_WORLD"), { "node_position_world" }, VisualShaderNode::PORT_TYPE_VECTOR_3D, TYPE_FLAGS_VERTEX, Shader::MODE_SPATIAL)); add_options.push_back(AddOption("PointSize", "Input/Vertex", "VisualShaderNodeInput", vformat(input_param_for_vertex_shader_mode, "point_size", "POINT_SIZE"), { "point_size" }, VisualShaderNode::PORT_TYPE_SCALAR, TYPE_FLAGS_VERTEX, Shader::MODE_SPATIAL)); add_options.push_back(AddOption("Tangent", "Input/Vertex", "VisualShaderNodeInput", vformat(input_param_for_vertex_and_fragment_shader_mode, "tangent", "TANGENT"), { "tangent" }, VisualShaderNode::PORT_TYPE_VECTOR_3D, TYPE_FLAGS_VERTEX, Shader::MODE_SPATIAL)); add_options.push_back(AddOption("Vertex", "Input/Vertex", "VisualShaderNodeInput", vformat(input_param_for_vertex_and_fragment_shader_modes, "vertex", "VERTEX"), { "vertex" }, VisualShaderNode::PORT_TYPE_VECTOR_3D, TYPE_FLAGS_VERTEX, Shader::MODE_SPATIAL)); @@ -6606,17 +6899,17 @@ VisualShaderEditor::VisualShaderEditor() { add_options.push_back(AddOption("ViewIndex", "Input/Vertex", "VisualShaderNodeInput", vformat(input_param_for_vertex_and_fragment_shader_modes, "view_index", "VIEW_INDEX"), { "view_index" }, VisualShaderNode::PORT_TYPE_SCALAR_INT, TYPE_FLAGS_VERTEX, Shader::MODE_SPATIAL)); add_options.push_back(AddOption("ViewMonoLeft", "Input/Vertex", "VisualShaderNodeInput", vformat(input_param_for_vertex_and_fragment_shader_modes, "view_mono_left", "VIEW_MONO_LEFT"), { "view_mono_left" }, VisualShaderNode::PORT_TYPE_SCALAR_INT, TYPE_FLAGS_VERTEX, Shader::MODE_SPATIAL)); add_options.push_back(AddOption("ViewRight", "Input/Vertex", "VisualShaderNodeInput", vformat(input_param_for_vertex_and_fragment_shader_modes, "view_right", "VIEW_RIGHT"), { "view_right" }, VisualShaderNode::PORT_TYPE_SCALAR_INT, TYPE_FLAGS_VERTEX, Shader::MODE_SPATIAL)); - add_options.push_back(AddOption("EyeOffset", "Input/Vertex", "VisualShaderNodeInput", vformat(input_param_for_vertex_and_fragment_shader_modes, "eye_offset", "EYE_OFFSET"), { "eye_offset" }, VisualShaderNode::PORT_TYPE_VECTOR_3D, TYPE_FLAGS_VERTEX, Shader::MODE_SPATIAL)); - add_options.push_back(AddOption("NodePositionWorld", "Input/Vertex", "VisualShaderNodeInput", vformat(input_param_for_vertex_and_fragment_shader_modes, "node_position_world", "NODE_POSITION_WORLD"), { "node_position_world" }, VisualShaderNode::PORT_TYPE_VECTOR_3D, TYPE_FLAGS_VERTEX, Shader::MODE_SPATIAL)); - add_options.push_back(AddOption("CameraPositionWorld", "Input/Vertex", "VisualShaderNodeInput", vformat(input_param_for_vertex_and_fragment_shader_modes, "camera_position_world", "CAMERA_POSITION_WORLD"), { "camera_position_world" }, VisualShaderNode::PORT_TYPE_VECTOR_3D, TYPE_FLAGS_VERTEX, Shader::MODE_SPATIAL)); - add_options.push_back(AddOption("CameraDirectionWorld", "Input/Vertex", "VisualShaderNodeInput", vformat(input_param_for_vertex_and_fragment_shader_modes, "camera_direction_world", "CAMERA_DIRECTION_WORLD"), { "camera_direction_world" }, VisualShaderNode::PORT_TYPE_VECTOR_3D, TYPE_FLAGS_VERTEX, Shader::MODE_SPATIAL)); - add_options.push_back(AddOption("CameraVisibleLayers", "Input/Vertex", "VisualShaderNodeInput", vformat(input_param_for_vertex_and_fragment_shader_modes, "camera_visible_layers", "CAMERA_VISIBLE_LAYERS"), { "camera_visible_layers" }, VisualShaderNode::PORT_TYPE_SCALAR_UINT, TYPE_FLAGS_VERTEX, Shader::MODE_SPATIAL)); - add_options.push_back(AddOption("NodePositionView", "Input/Vertex", "VisualShaderNodeInput", vformat(input_param_for_vertex_and_fragment_shader_modes, "node_position_view", "NODE_POSITION_VIEW"), { "node_position_view" }, VisualShaderNode::PORT_TYPE_VECTOR_3D, TYPE_FLAGS_VERTEX, Shader::MODE_SPATIAL)); add_options.push_back(AddOption("Binormal", "Input/Fragment", "VisualShaderNodeInput", vformat(input_param_for_vertex_and_fragment_shader_modes, "binormal", "BINORMAL"), { "binormal" }, VisualShaderNode::PORT_TYPE_VECTOR_3D, TYPE_FLAGS_FRAGMENT, Shader::MODE_SPATIAL)); + add_options.push_back(AddOption("CameraDirectionWorld", "Input/Fragment", "VisualShaderNodeInput", vformat(input_param_for_vertex_and_fragment_shader_modes, "camera_direction_world", "CAMERA_DIRECTION_WORLD"), { "camera_direction_world" }, VisualShaderNode::PORT_TYPE_VECTOR_3D, TYPE_FLAGS_FRAGMENT, Shader::MODE_SPATIAL)); + add_options.push_back(AddOption("CameraPositionWorld", "Input/Fragment", "VisualShaderNodeInput", vformat(input_param_for_vertex_and_fragment_shader_modes, "camera_position_world", "CAMERA_POSITION_WORLD"), { "camera_position_world" }, VisualShaderNode::PORT_TYPE_VECTOR_3D, TYPE_FLAGS_FRAGMENT, Shader::MODE_SPATIAL)); + add_options.push_back(AddOption("CameraVisibleLayers", "Input/Fragment", "VisualShaderNodeInput", vformat(input_param_for_vertex_and_fragment_shader_modes, "camera_visible_layers", "CAMERA_VISIBLE_LAYERS"), { "camera_visible_layers" }, VisualShaderNode::PORT_TYPE_SCALAR_UINT, TYPE_FLAGS_FRAGMENT, Shader::MODE_SPATIAL)); add_options.push_back(AddOption("Color", "Input/Fragment", "VisualShaderNodeInput", vformat(input_param_for_vertex_and_fragment_shader_modes, "color", "COLOR"), { "color" }, VisualShaderNode::PORT_TYPE_VECTOR_4D, TYPE_FLAGS_FRAGMENT, Shader::MODE_SPATIAL)); + add_options.push_back(AddOption("EyeOffset", "Input/Fragment", "VisualShaderNodeInput", vformat(input_param_for_vertex_and_fragment_shader_modes, "eye_offset", "EYE_OFFSET"), { "eye_offset" }, VisualShaderNode::PORT_TYPE_VECTOR_3D, TYPE_FLAGS_FRAGMENT, Shader::MODE_SPATIAL)); add_options.push_back(AddOption("FragCoord", "Input/Fragment", "VisualShaderNodeInput", vformat(input_param_for_fragment_and_light_shader_modes, "fragcoord", "FRAGCOORD"), { "fragcoord" }, VisualShaderNode::PORT_TYPE_VECTOR_4D, TYPE_FLAGS_FRAGMENT, Shader::MODE_SPATIAL)); add_options.push_back(AddOption("FrontFacing", "Input/Fragment", "VisualShaderNodeInput", vformat(input_param_for_fragment_shader_mode, "front_facing", "FRONT_FACING"), { "front_facing" }, VisualShaderNode::PORT_TYPE_BOOLEAN, TYPE_FLAGS_FRAGMENT, Shader::MODE_SPATIAL)); + add_options.push_back(AddOption("NodePositionView", "Input/Fragment", "VisualShaderNodeInput", vformat(input_param_for_vertex_and_fragment_shader_modes, "node_position_view", "NODE_POSITION_VIEW"), { "node_position_view" }, VisualShaderNode::PORT_TYPE_VECTOR_3D, TYPE_FLAGS_FRAGMENT, Shader::MODE_SPATIAL)); + add_options.push_back(AddOption("NodePositionWorld", "Input/Fragment", "VisualShaderNodeInput", vformat(input_param_for_vertex_and_fragment_shader_modes, "node_position_world", "NODE_POSITION_WORLD"), { "node_position_world" }, VisualShaderNode::PORT_TYPE_VECTOR_3D, TYPE_FLAGS_FRAGMENT, Shader::MODE_SPATIAL)); add_options.push_back(AddOption("PointCoord", "Input/Fragment", "VisualShaderNodeInput", vformat(input_param_for_fragment_shader_mode, "point_coord", "POINT_COORD"), { "point_coord" }, VisualShaderNode::PORT_TYPE_VECTOR_2D, TYPE_FLAGS_FRAGMENT, Shader::MODE_SPATIAL)); add_options.push_back(AddOption("ScreenUV", "Input/Fragment", "VisualShaderNodeInput", vformat(input_param_for_fragment_shader_mode, "screen_uv", "SCREEN_UV"), { "screen_uv" }, VisualShaderNode::PORT_TYPE_VECTOR_2D, TYPE_FLAGS_FRAGMENT, Shader::MODE_SPATIAL)); add_options.push_back(AddOption("Tangent", "Input/Fragment", "VisualShaderNodeInput", vformat(input_param_for_vertex_and_fragment_shader_modes, "tangent", "TANGENT"), { "tangent" }, VisualShaderNode::PORT_TYPE_VECTOR_3D, TYPE_FLAGS_FRAGMENT, Shader::MODE_SPATIAL)); @@ -6625,12 +6918,6 @@ VisualShaderEditor::VisualShaderEditor() { add_options.push_back(AddOption("ViewIndex", "Input/Fragment", "VisualShaderNodeInput", vformat(input_param_for_vertex_and_fragment_shader_modes, "view_index", "VIEW_INDEX"), { "view_index" }, VisualShaderNode::PORT_TYPE_SCALAR_INT, TYPE_FLAGS_FRAGMENT, Shader::MODE_SPATIAL)); add_options.push_back(AddOption("ViewMonoLeft", "Input/Fragment", "VisualShaderNodeInput", vformat(input_param_for_vertex_and_fragment_shader_modes, "view_mono_left", "VIEW_MONO_LEFT"), { "view_mono_left" }, VisualShaderNode::PORT_TYPE_SCALAR_INT, TYPE_FLAGS_FRAGMENT, Shader::MODE_SPATIAL)); add_options.push_back(AddOption("ViewRight", "Input/Fragment", "VisualShaderNodeInput", vformat(input_param_for_vertex_and_fragment_shader_modes, "view_right", "VIEW_RIGHT"), { "view_right" }, VisualShaderNode::PORT_TYPE_SCALAR_INT, TYPE_FLAGS_FRAGMENT, Shader::MODE_SPATIAL)); - add_options.push_back(AddOption("EyeOffset", "Input/Fragment", "VisualShaderNodeInput", vformat(input_param_for_vertex_and_fragment_shader_modes, "eye_offset", "EYE_OFFSET"), { "eye_offset" }, VisualShaderNode::PORT_TYPE_VECTOR_3D, TYPE_FLAGS_FRAGMENT, Shader::MODE_SPATIAL)); - add_options.push_back(AddOption("NodePositionWorld", "Input/Fragment", "VisualShaderNodeInput", vformat(input_param_for_vertex_and_fragment_shader_modes, "node_position_world", "NODE_POSITION_WORLD"), { "node_position_world" }, VisualShaderNode::PORT_TYPE_VECTOR_3D, TYPE_FLAGS_FRAGMENT, Shader::MODE_SPATIAL)); - add_options.push_back(AddOption("CameraPositionWorld", "Input/Fragment", "VisualShaderNodeInput", vformat(input_param_for_vertex_and_fragment_shader_modes, "camera_position_world", "CAMERA_POSITION_WORLD"), { "camera_position_world" }, VisualShaderNode::PORT_TYPE_VECTOR_3D, TYPE_FLAGS_FRAGMENT, Shader::MODE_SPATIAL)); - add_options.push_back(AddOption("CameraDirectionWorld", "Input/Fragment", "VisualShaderNodeInput", vformat(input_param_for_vertex_and_fragment_shader_modes, "camera_direction_world", "CAMERA_DIRECTION_WORLD"), { "camera_direction_world" }, VisualShaderNode::PORT_TYPE_VECTOR_3D, TYPE_FLAGS_FRAGMENT, Shader::MODE_SPATIAL)); - add_options.push_back(AddOption("CameraVisibleLayers", "Input/Fragment", "VisualShaderNodeInput", vformat(input_param_for_vertex_and_fragment_shader_modes, "camera_visible_layers", "CAMERA_VISIBLE_LAYERS"), { "camera_visible_layers" }, VisualShaderNode::PORT_TYPE_SCALAR_UINT, TYPE_FLAGS_FRAGMENT, Shader::MODE_SPATIAL)); - add_options.push_back(AddOption("NodePositionView", "Input/Fragment", "VisualShaderNodeInput", vformat(input_param_for_vertex_and_fragment_shader_modes, "node_position_view", "NODE_POSITION_VIEW"), { "node_position_view" }, VisualShaderNode::PORT_TYPE_VECTOR_3D, TYPE_FLAGS_FRAGMENT, Shader::MODE_SPATIAL)); add_options.push_back(AddOption("Albedo", "Input/Light", "VisualShaderNodeInput", vformat(input_param_for_light_shader_mode, "albedo", "ALBEDO"), { "albedo" }, VisualShaderNode::PORT_TYPE_VECTOR_3D, TYPE_FLAGS_LIGHT, Shader::MODE_SPATIAL)); add_options.push_back(AddOption("Attenuation", "Input/Light", "VisualShaderNodeInput", vformat(input_param_for_light_shader_mode, "attenuation", "ATTENUATION"), { "attenuation" }, VisualShaderNode::PORT_TYPE_SCALAR, TYPE_FLAGS_LIGHT, Shader::MODE_SPATIAL)); @@ -6673,10 +6960,10 @@ VisualShaderEditor::VisualShaderEditor() { add_options.push_back(AddOption("FragCoord", "Input/Light", "VisualShaderNodeInput", vformat(input_param_for_fragment_and_light_shader_modes, "fragcoord", "FRAGCOORD"), { "fragcoord" }, VisualShaderNode::PORT_TYPE_VECTOR_4D, TYPE_FLAGS_LIGHT, Shader::MODE_CANVAS_ITEM)); add_options.push_back(AddOption("Light", "Input/Light", "VisualShaderNodeInput", vformat(input_param_for_light_shader_mode, "light", "LIGHT"), { "light" }, VisualShaderNode::PORT_TYPE_VECTOR_4D, TYPE_FLAGS_LIGHT, Shader::MODE_CANVAS_ITEM)); add_options.push_back(AddOption("LightColor", "Input/Light", "VisualShaderNodeInput", vformat(input_param_for_light_shader_mode, "light_color", "LIGHT_COLOR"), { "light_color" }, VisualShaderNode::PORT_TYPE_VECTOR_4D, TYPE_FLAGS_LIGHT, Shader::MODE_CANVAS_ITEM)); - add_options.push_back(AddOption("LightPosition", "Input/Light", "VisualShaderNodeInput", vformat(input_param_for_light_shader_mode, "light_position", "LIGHT_POSITION"), { "light_position" }, VisualShaderNode::PORT_TYPE_VECTOR_3D, TYPE_FLAGS_LIGHT, Shader::MODE_CANVAS_ITEM)); add_options.push_back(AddOption("LightDirection", "Input/Light", "VisualShaderNodeInput", vformat(input_param_for_light_shader_mode, "light_direction", "LIGHT_DIRECTION"), { "light_direction" }, VisualShaderNode::PORT_TYPE_VECTOR_3D, TYPE_FLAGS_LIGHT, Shader::MODE_CANVAS_ITEM)); - add_options.push_back(AddOption("LightIsDirectional", "Input/Light", "VisualShaderNodeInput", vformat(input_param_for_light_shader_mode, "light_is_directional", "LIGHT_IS_DIRECTIONAL"), { "light_is_directional" }, VisualShaderNode::PORT_TYPE_BOOLEAN, TYPE_FLAGS_LIGHT, Shader::MODE_CANVAS_ITEM)); add_options.push_back(AddOption("LightEnergy", "Input/Light", "VisualShaderNodeInput", vformat(input_param_for_light_shader_mode, "light_energy", "LIGHT_ENERGY"), { "light_energy" }, VisualShaderNode::PORT_TYPE_SCALAR, TYPE_FLAGS_LIGHT, Shader::MODE_CANVAS_ITEM)); + add_options.push_back(AddOption("LightIsDirectional", "Input/Light", "VisualShaderNodeInput", vformat(input_param_for_light_shader_mode, "light_is_directional", "LIGHT_IS_DIRECTIONAL"), { "light_is_directional" }, VisualShaderNode::PORT_TYPE_BOOLEAN, TYPE_FLAGS_LIGHT, Shader::MODE_CANVAS_ITEM)); + add_options.push_back(AddOption("LightPosition", "Input/Light", "VisualShaderNodeInput", vformat(input_param_for_light_shader_mode, "light_position", "LIGHT_POSITION"), { "light_position" }, VisualShaderNode::PORT_TYPE_VECTOR_3D, TYPE_FLAGS_LIGHT, Shader::MODE_CANVAS_ITEM)); add_options.push_back(AddOption("LightVertex", "Input/Light", "VisualShaderNodeInput", vformat(input_param_for_fragment_and_light_shader_modes, "light_vertex", "LIGHT_VERTEX"), { "light_vertex" }, VisualShaderNode::PORT_TYPE_VECTOR_3D, TYPE_FLAGS_LIGHT, Shader::MODE_CANVAS_ITEM)); add_options.push_back(AddOption("Normal", "Input/Light", "VisualShaderNodeInput", vformat(input_param_for_light_shader_mode, "normal", "NORMAL"), { "normal" }, VisualShaderNode::PORT_TYPE_VECTOR_3D, TYPE_FLAGS_LIGHT, Shader::MODE_CANVAS_ITEM)); add_options.push_back(AddOption("PointCoord", "Input/Light", "VisualShaderNodeInput", vformat(input_param_for_fragment_and_light_shader_modes, "point_coord", "POINT_COORD"), { "point_coord" }, VisualShaderNode::PORT_TYPE_VECTOR_2D, TYPE_FLAGS_LIGHT, Shader::MODE_CANVAS_ITEM)); @@ -6691,6 +6978,7 @@ VisualShaderEditor::VisualShaderEditor() { add_options.push_back(AddOption("AtHalfResPass", "Input/Sky", "VisualShaderNodeInput", vformat(input_param_for_sky_shader_mode, "at_half_res_pass", "AT_HALF_RES_PASS"), { "at_half_res_pass" }, VisualShaderNode::PORT_TYPE_BOOLEAN, TYPE_FLAGS_SKY, Shader::MODE_SKY)); add_options.push_back(AddOption("AtQuarterResPass", "Input/Sky", "VisualShaderNodeInput", vformat(input_param_for_sky_shader_mode, "at_quarter_res_pass", "AT_QUARTER_RES_PASS"), { "at_quarter_res_pass" }, VisualShaderNode::PORT_TYPE_BOOLEAN, TYPE_FLAGS_SKY, Shader::MODE_SKY)); add_options.push_back(AddOption("EyeDir", "Input/Sky", "VisualShaderNodeInput", vformat(input_param_for_sky_shader_mode, "eyedir", "EYEDIR"), { "eyedir" }, VisualShaderNode::PORT_TYPE_VECTOR_3D, TYPE_FLAGS_SKY, Shader::MODE_SKY)); + add_options.push_back(AddOption("FragCoord", "Input/Sky", "VisualShaderNodeInput", vformat(input_param_for_sky_shader_mode, "fragcoord", "FRAGCOORD"), { "fragcoord" }, VisualShaderNode::PORT_TYPE_VECTOR_4D, TYPE_FLAGS_SKY, Shader::MODE_SKY)); add_options.push_back(AddOption("HalfResColor", "Input/Sky", "VisualShaderNodeInput", vformat(input_param_for_sky_shader_mode, "half_res_color", "HALF_RES_COLOR"), { "half_res_color" }, VisualShaderNode::PORT_TYPE_VECTOR_4D, TYPE_FLAGS_SKY, Shader::MODE_SKY)); add_options.push_back(AddOption("Light0Color", "Input/Sky", "VisualShaderNodeInput", vformat(input_param_for_sky_shader_mode, "light0_color", "LIGHT0_COLOR"), { "light0_color" }, VisualShaderNode::PORT_TYPE_VECTOR_3D, TYPE_FLAGS_SKY, Shader::MODE_SKY)); add_options.push_back(AddOption("Light0Direction", "Input/Sky", "VisualShaderNodeInput", vformat(input_param_for_sky_shader_mode, "light0_direction", "LIGHT0_DIRECTION"), { "light0_direction" }, VisualShaderNode::PORT_TYPE_VECTOR_3D, TYPE_FLAGS_SKY, Shader::MODE_SKY)); @@ -6712,19 +7000,17 @@ VisualShaderEditor::VisualShaderEditor() { add_options.push_back(AddOption("QuarterResColor", "Input/Sky", "VisualShaderNodeInput", vformat(input_param_for_sky_shader_mode, "quarter_res_color", "QUARTER_RES_COLOR"), { "quarter_res_color" }, VisualShaderNode::PORT_TYPE_VECTOR_4D, TYPE_FLAGS_SKY, Shader::MODE_SKY)); add_options.push_back(AddOption("Radiance", "Input/Sky", "VisualShaderNodeInput", vformat(input_param_for_sky_shader_mode, "radiance", "RADIANCE"), { "radiance" }, VisualShaderNode::PORT_TYPE_SAMPLER, TYPE_FLAGS_SKY, Shader::MODE_SKY)); add_options.push_back(AddOption("ScreenUV", "Input/Sky", "VisualShaderNodeInput", vformat(input_param_for_sky_shader_mode, "screen_uv", "SCREEN_UV"), { "screen_uv" }, VisualShaderNode::PORT_TYPE_VECTOR_2D, TYPE_FLAGS_SKY, Shader::MODE_SKY)); - add_options.push_back(AddOption("FragCoord", "Input/Sky", "VisualShaderNodeInput", vformat(input_param_for_sky_shader_mode, "fragcoord", "FRAGCOORD"), { "fragcoord" }, VisualShaderNode::PORT_TYPE_VECTOR_4D, TYPE_FLAGS_SKY, Shader::MODE_SKY)); - add_options.push_back(AddOption("SkyCoords", "Input/Sky", "VisualShaderNodeInput", vformat(input_param_for_sky_shader_mode, "sky_coords", "SKY_COORDS"), { "sky_coords" }, VisualShaderNode::PORT_TYPE_VECTOR_2D, TYPE_FLAGS_SKY, Shader::MODE_SKY)); add_options.push_back(AddOption("Time", "Input/Sky", "VisualShaderNodeInput", vformat(input_param_for_sky_shader_mode, "time", "TIME"), { "time" }, VisualShaderNode::PORT_TYPE_SCALAR, TYPE_FLAGS_SKY, Shader::MODE_SKY)); // FOG INPUTS - add_options.push_back(AddOption("WorldPosition", "Input/Fog", "VisualShaderNodeInput", vformat(input_param_for_fog_shader_mode, "world_position", "WORLD_POSITION"), { "world_position" }, VisualShaderNode::PORT_TYPE_VECTOR_3D, TYPE_FLAGS_FOG, Shader::MODE_FOG)); add_options.push_back(AddOption("ObjectPosition", "Input/Fog", "VisualShaderNodeInput", vformat(input_param_for_fog_shader_mode, "object_position", "OBJECT_POSITION"), { "object_position" }, VisualShaderNode::PORT_TYPE_VECTOR_3D, TYPE_FLAGS_FOG, Shader::MODE_FOG)); - add_options.push_back(AddOption("UVW", "Input/Fog", "VisualShaderNodeInput", vformat(input_param_for_fog_shader_mode, "uvw", "UVW"), { "uvw" }, VisualShaderNode::PORT_TYPE_VECTOR_3D, TYPE_FLAGS_FOG, Shader::MODE_FOG)); - add_options.push_back(AddOption("Size", "Input/Fog", "VisualShaderNodeInput", vformat(input_param_for_fog_shader_mode, "size", "SIZE"), { "size" }, VisualShaderNode::PORT_TYPE_VECTOR_3D, TYPE_FLAGS_FOG, Shader::MODE_FOG)); add_options.push_back(AddOption("SDF", "Input/Fog", "VisualShaderNodeInput", vformat(input_param_for_fog_shader_mode, "sdf", "SDF"), { "sdf" }, VisualShaderNode::PORT_TYPE_SCALAR, TYPE_FLAGS_FOG, Shader::MODE_FOG)); + add_options.push_back(AddOption("Size", "Input/Fog", "VisualShaderNodeInput", vformat(input_param_for_fog_shader_mode, "size", "SIZE"), { "size" }, VisualShaderNode::PORT_TYPE_VECTOR_3D, TYPE_FLAGS_FOG, Shader::MODE_FOG)); add_options.push_back(AddOption("Time", "Input/Fog", "VisualShaderNodeInput", vformat(input_param_for_fog_shader_mode, "time", "TIME"), { "time" }, VisualShaderNode::PORT_TYPE_SCALAR, TYPE_FLAGS_FOG, Shader::MODE_FOG)); + add_options.push_back(AddOption("UVW", "Input/Fog", "VisualShaderNodeInput", vformat(input_param_for_fog_shader_mode, "uvw", "UVW"), { "uvw" }, VisualShaderNode::PORT_TYPE_VECTOR_3D, TYPE_FLAGS_FOG, Shader::MODE_FOG)); + add_options.push_back(AddOption("WorldPosition", "Input/Fog", "VisualShaderNodeInput", vformat(input_param_for_fog_shader_mode, "world_position", "WORLD_POSITION"), { "world_position" }, VisualShaderNode::PORT_TYPE_VECTOR_3D, TYPE_FLAGS_FOG, Shader::MODE_FOG)); // PARTICLES INPUTS @@ -6739,7 +7025,10 @@ VisualShaderEditor::VisualShaderEditor() { add_options.push_back(AddOption("MultiplyByAxisAngle (*)", "Particles/Transform", "VisualShaderNodeParticleMultiplyByAxisAngle", TTR("A node for help to multiply a position input vector by rotation using specific axis. Intended to work with emitters."), {}, VisualShaderNode::PORT_TYPE_VECTOR_3D, TYPE_FLAGS_EMIT | TYPE_FLAGS_PROCESS | TYPE_FLAGS_COLLIDE, Shader::MODE_PARTICLES)); add_options.push_back(AddOption("BoxEmitter", "Particles/Emitters", "VisualShaderNodeParticleBoxEmitter", "", {}, VisualShaderNode::PORT_TYPE_VECTOR_3D, TYPE_FLAGS_EMIT, Shader::MODE_PARTICLES)); + + mesh_emitter_option_idx = add_options.size(); add_options.push_back(AddOption("MeshEmitter", "Particles/Emitters", "VisualShaderNodeParticleMeshEmitter", "", {}, VisualShaderNode::PORT_TYPE_VECTOR_3D, TYPE_FLAGS_EMIT, Shader::MODE_PARTICLES)); + add_options.push_back(AddOption("RingEmitter", "Particles/Emitters", "VisualShaderNodeParticleRingEmitter", "", {}, VisualShaderNode::PORT_TYPE_VECTOR_3D, TYPE_FLAGS_EMIT, Shader::MODE_PARTICLES)); add_options.push_back(AddOption("SphereEmitter", "Particles/Emitters", "VisualShaderNodeParticleSphereEmitter", "", {}, VisualShaderNode::PORT_TYPE_VECTOR_3D, TYPE_FLAGS_EMIT, Shader::MODE_PARTICLES)); @@ -7705,7 +7994,7 @@ void VisualShaderNodePortPreview::_shader_changed() { preview_shader->set_code(shader_code); for (int i = 0; i < default_textures.size(); i++) { int j = 0; - for (List<Ref<Texture2D>>::ConstIterator itr = default_textures[i].params.begin(); itr != default_textures[i].params.end(); ++itr, ++j) { + for (List<Ref<Texture>>::ConstIterator itr = default_textures[i].params.begin(); itr != default_textures[i].params.end(); ++itr, ++j) { preview_shader->set_default_texture_parameter(default_textures[i].name, *itr, j); } } @@ -7714,36 +8003,21 @@ void VisualShaderNodePortPreview::_shader_changed() { mat.instantiate(); mat->set_shader(preview_shader); - //find if a material is also being edited and copy parameters to this one - - for (int i = EditorNode::get_singleton()->get_editor_selection_history()->get_path_size() - 1; i >= 0; i--) { - Object *object = ObjectDB::get_instance(EditorNode::get_singleton()->get_editor_selection_history()->get_path_object(i)); - ShaderMaterial *src_mat; - if (!object) { - continue; - } - if (object->has_method("get_material_override")) { // trying getting material from MeshInstance - src_mat = Object::cast_to<ShaderMaterial>(object->call("get_material_override")); - } else if (object->has_method("get_material")) { // from CanvasItem/Node2D - src_mat = Object::cast_to<ShaderMaterial>(object->call("get_material")); - } else { - src_mat = Object::cast_to<ShaderMaterial>(object); - } - if (src_mat && src_mat->get_shader().is_valid()) { - List<PropertyInfo> params; - src_mat->get_shader()->get_shader_uniform_list(¶ms); - for (const PropertyInfo &E : params) { - mat->set_shader_parameter(E.name, src_mat->get_shader_parameter(E.name)); - } + if (preview_mat.is_valid() && preview_mat->get_shader().is_valid()) { + List<PropertyInfo> params; + preview_mat->get_shader()->get_shader_uniform_list(¶ms); + for (const PropertyInfo &E : params) { + mat->set_shader_parameter(E.name, preview_mat->get_shader_parameter(E.name)); } } set_material(mat); } -void VisualShaderNodePortPreview::setup(const Ref<VisualShader> &p_shader, VisualShader::Type p_type, int p_node, int p_port, bool p_is_valid) { +void VisualShaderNodePortPreview::setup(const Ref<VisualShader> &p_shader, Ref<ShaderMaterial> &p_preview_material, VisualShader::Type p_type, int p_node, int p_port, bool p_is_valid) { shader = p_shader; shader->connect_changed(callable_mp(this, &VisualShaderNodePortPreview::_shader_changed), CONNECT_DEFERRED); + preview_mat = p_preview_material; type = p_type; port = p_port; node = p_node; diff --git a/editor/plugins/visual_shader_editor_plugin.h b/editor/plugins/visual_shader_editor_plugin.h index 6ce096f821..69b2f30c40 100644 --- a/editor/plugins/visual_shader_editor_plugin.h +++ b/editor/plugins/visual_shader_editor_plugin.h @@ -50,6 +50,7 @@ class RichTextLabel; class Tree; class VisualShaderEditor; +class MaterialEditor; class VisualShaderNodePlugin : public RefCounted { GDCLASS(VisualShaderNodePlugin, RefCounted); @@ -206,11 +207,18 @@ class VisualShaderEditor : public ShaderEditor { int editing_port = -1; Ref<VisualShaderEditedProperty> edited_property_holder; + MaterialEditor *material_editor = nullptr; Ref<VisualShader> visual_shader; + Ref<ShaderMaterial> preview_material; + Ref<Environment> env; + String param_filter_name; + EditorProperty *current_prop = nullptr; + VBoxContainer *shader_preview_vbox = nullptr; GraphEdit *graph = nullptr; Button *add_node = nullptr; MenuButton *varying_button = nullptr; - Button *preview_shader = nullptr; + Button *code_preview_button = nullptr; + Button *shader_preview_button = nullptr; OptionButton *edit_type = nullptr; OptionButton *edit_type_standard = nullptr; @@ -222,8 +230,8 @@ class VisualShaderEditor : public ShaderEditor { bool pending_update_preview = false; bool shader_error = false; - Window *preview_window = nullptr; - VBoxContainer *preview_vbox = nullptr; + Window *code_preview_window = nullptr; + VBoxContainer *code_preview_vbox = nullptr; CodeEdit *preview_text = nullptr; Ref<CodeHighlighter> syntax_highlighter = nullptr; PanelContainer *error_panel = nullptr; @@ -261,8 +269,17 @@ class VisualShaderEditor : public ShaderEditor { PopupPanel *frame_tint_color_pick_popup = nullptr; ColorPicker *frame_tint_color_picker = nullptr; - bool preview_first = true; - bool preview_showed = false; + bool code_preview_first = true; + bool code_preview_showed = false; + + bool shader_preview_showed = true; + + LineEdit *param_filter = nullptr; + String selected_param_id; + Tree *parameters = nullptr; + HashMap<String, PropertyInfo> parameter_props; + VBoxContainer *param_vbox = nullptr; + VBoxContainer *param_vbox2 = nullptr; enum ShaderModeFlags { MODE_FLAGS_SPATIAL_CANVASITEM = 1, @@ -349,6 +366,10 @@ class VisualShaderEditor : public ShaderEditor { void _show_add_varying_dialog(); void _show_remove_varying_dialog(); + void _clear_preview_param(); + void _update_preview_parameter_list(); + bool _update_preview_parameter_tree(); + void _update_nodes(); void _update_graph(); @@ -393,6 +414,7 @@ class VisualShaderEditor : public ShaderEditor { int custom_node_option_idx; int curve_node_option_idx; int curve_xyz_node_option_idx; + int mesh_emitter_option_idx; List<String> keyword_list; List<VisualShaderNodeParameterRef> uniform_refs; @@ -414,6 +436,8 @@ class VisualShaderEditor : public ShaderEditor { void _get_next_nodes_recursively(VisualShader::Type p_type, int p_node_id, LocalVector<int> &r_nodes) const; String _get_description(int p_idx); + void _show_shader_preview(); + Vector<int> nodes_link_to_frame_buffer; // Contains the nodes that are requested to be linked to a frame. This is used to perform one Undo/Redo operation for dragging nodes. int frame_node_id_to_link_to = -1; @@ -592,6 +616,12 @@ class VisualShaderEditor : public ShaderEditor { void _resource_removed(const Ref<Resource> &p_resource); void _resources_removed(); + void _param_property_changed(const String &p_property, const Variant &p_value, const String &p_field = "", bool p_changing = false); + void _update_current_param(); + void _param_filter_changed(const String &p_text); + void _param_selected(); + void _param_unselected(); + protected: void _notification(int p_what); static void _bind_methods(); @@ -652,6 +682,7 @@ public: class VisualShaderNodePortPreview : public Control { GDCLASS(VisualShaderNodePortPreview, Control); Ref<VisualShader> shader; + Ref<ShaderMaterial> preview_mat; VisualShader::Type type = VisualShader::Type::TYPE_MAX; int node = 0; int port = 0; @@ -662,7 +693,7 @@ protected: public: virtual Size2 get_minimum_size() const override; - void setup(const Ref<VisualShader> &p_shader, VisualShader::Type p_type, int p_node, int p_port, bool p_is_valid); + void setup(const Ref<VisualShader> &p_shader, Ref<ShaderMaterial> &p_preview_material, VisualShader::Type p_type, int p_node, int p_port, bool p_is_valid); }; class VisualShaderConversionPlugin : public EditorResourceConversionPlugin { diff --git a/editor/progress_dialog.cpp b/editor/progress_dialog.cpp index c21723d1ba..2f345e5161 100644 --- a/editor/progress_dialog.cpp +++ b/editor/progress_dialog.cpp @@ -202,14 +202,13 @@ void ProgressDialog::add_task(const String &p_task, const String &p_label, int p bool ProgressDialog::task_step(const String &p_task, const String &p_state, int p_step, bool p_force_redraw) { ERR_FAIL_COND_V(!tasks.has(p_task), canceled); + Task &t = tasks[p_task]; if (!p_force_redraw) { uint64_t tus = OS::get_singleton()->get_ticks_usec(); - if (tus - last_progress_tick < 200000) { //200ms + if (tus - t.last_progress_tick < 200000) { //200ms return canceled; } } - - Task &t = tasks[p_task]; if (p_step < 0) { t.progress->set_value(t.progress->get_value() + 1); } else { @@ -217,7 +216,7 @@ bool ProgressDialog::task_step(const String &p_task, const String &p_state, int } t.state->set_text(p_state); - last_progress_tick = OS::get_singleton()->get_ticks_usec(); + t.last_progress_tick = OS::get_singleton()->get_ticks_usec(); _update_ui(); return canceled; @@ -252,7 +251,6 @@ ProgressDialog::ProgressDialog() { main->set_anchors_and_offsets_preset(Control::PRESET_FULL_RECT); set_exclusive(true); set_flag(Window::FLAG_POPUP, false); - last_progress_tick = 0; singleton = this; cancel_hb = memnew(HBoxContainer); main->add_child(cancel_hb); diff --git a/editor/progress_dialog.h b/editor/progress_dialog.h index 82d59219da..355812b0b7 100644 --- a/editor/progress_dialog.h +++ b/editor/progress_dialog.h @@ -71,13 +71,13 @@ class ProgressDialog : public PopupPanel { VBoxContainer *vb = nullptr; ProgressBar *progress = nullptr; Label *state = nullptr; + uint64_t last_progress_tick = 0; }; HBoxContainer *cancel_hb = nullptr; Button *cancel = nullptr; HashMap<String, Task> tasks; VBoxContainer *main = nullptr; - uint64_t last_progress_tick; LocalVector<Window *> host_windows; diff --git a/editor/project_manager.cpp b/editor/project_manager.cpp index 3de9ebcfdd..55c361de4b 100644 --- a/editor/project_manager.cpp +++ b/editor/project_manager.cpp @@ -93,6 +93,7 @@ void ProjectManager::_notification(int p_what) { } break; case NOTIFICATION_READY: { + DisplayServer::get_singleton()->screen_set_keep_on(EDITOR_GET("interface/editor/keep_screen_on")); const int default_sorting = (int)EDITOR_GET("project_manager/sorting_order"); filter_option->select(default_sorting); project_list->set_order_option(default_sorting); diff --git a/editor/project_manager/project_dialog.cpp b/editor/project_manager/project_dialog.cpp index 28d2362c9c..9b009c0a89 100644 --- a/editor/project_manager/project_dialog.cpp +++ b/editor/project_manager/project_dialog.cpp @@ -857,7 +857,7 @@ ProjectDialog::ProjectDialog() { create_dir->set_text(TTR("Create Folder")); create_dir->set_pressed(true); pphb_label->add_child(create_dir); - create_dir->connect("toggled", callable_mp(this, &ProjectDialog::_create_dir_toggled)); + create_dir->connect(SceneStringName(toggled), callable_mp(this, &ProjectDialog::_create_dir_toggled)); HBoxContainer *pphb = memnew(HBoxContainer); project_path_container->add_child(pphb); diff --git a/editor/project_manager/project_list.cpp b/editor/project_manager/project_list.cpp index 092a6a1a18..541ab01e62 100644 --- a/editor/project_manager/project_list.cpp +++ b/editor/project_manager/project_list.cpp @@ -760,7 +760,7 @@ void ProjectList::_create_project_item_control(int p_index) { hb->set_tags(item.tags, this); hb->set_unsupported_features(item.unsupported_features.duplicate()); hb->set_project_version(item.project_version); - hb->set_last_edited_info(Time::get_singleton()->get_datetime_string_from_unix_time(item.last_edited, true)); + hb->set_last_edited_info(!item.missing ? Time::get_singleton()->get_datetime_string_from_unix_time(item.last_edited, true) : TTR("Missing Date")); hb->set_is_favorite(item.favorite); hb->set_is_missing(item.missing); diff --git a/editor/project_settings_editor.cpp b/editor/project_settings_editor.cpp index bdf4e41c5f..489fbb037f 100644 --- a/editor/project_settings_editor.cpp +++ b/editor/project_settings_editor.cpp @@ -221,7 +221,7 @@ void ProjectSettingsEditor::_update_property_box() { const Vector<String> names = name.split("/"); for (int i = 0; i < names.size(); i++) { - if (!names[i].is_valid_identifier()) { + if (!names[i].is_valid_ascii_identifier()) { return; } } @@ -652,7 +652,7 @@ ProjectSettingsEditor::ProjectSettingsEditor(EditorData *p_data) { advanced = memnew(CheckButton); advanced->set_text(TTR("Advanced Settings")); - advanced->connect("toggled", callable_mp(this, &ProjectSettingsEditor::_advanced_toggled)); + advanced->connect(SceneStringName(toggled), callable_mp(this, &ProjectSettingsEditor::_advanced_toggled)); search_bar->add_child(advanced); custom_properties = memnew(HBoxContainer); diff --git a/editor/property_selector.cpp b/editor/property_selector.cpp index d47270841d..8f609850b8 100644 --- a/editor/property_selector.cpp +++ b/editor/property_selector.cpp @@ -162,6 +162,9 @@ void PropertySelector::_update_search() { if (!found && !search_box->get_text().is_empty() && E.name.containsn(search_text)) { item->select(0); found = true; + } else if (!found && search_box->get_text().is_empty() && E.name == selected) { + item->select(0); + found = true; } item->set_selectable(0, true); @@ -173,6 +176,12 @@ void PropertySelector::_update_search() { if (category && category->get_first_child() == nullptr) { memdelete(category); //old category was unused } + + if (found) { + // As we call this while adding items, defer until list is completely populated. + callable_mp(search_options, &Tree::scroll_to_item).call_deferred(search_options->get_selected(), true); + } + } else { List<MethodInfo> methods; @@ -305,12 +314,20 @@ void PropertySelector::_update_search() { if (!found && !search_box->get_text().is_empty() && name.containsn(search_text)) { item->select(0); found = true; + } else if (!found && search_box->get_text().is_empty() && name == selected) { + item->select(0); + found = true; } } if (category && category->get_first_child() == nullptr) { memdelete(category); //old category was unused } + + if (found) { + // As we call this while adding items, defer until list is completely populated. + callable_mp(search_options, &Tree::scroll_to_item).call_deferred(search_options->get_selected(), true); + } } get_ok_button()->set_disabled(root->get_first_child() == nullptr); diff --git a/editor/register_editor_types.cpp b/editor/register_editor_types.cpp index 610ad3efdf..8e135e7eae 100644 --- a/editor/register_editor_types.cpp +++ b/editor/register_editor_types.cpp @@ -46,6 +46,7 @@ #include "editor/editor_translation_parser.h" #include "editor/editor_undo_redo_manager.h" #include "editor/export/editor_export_platform.h" +#include "editor/export/editor_export_platform_extension.h" #include "editor/export/editor_export_platform_pc.h" #include "editor/export/editor_export_plugin.h" #include "editor/filesystem_dock.h" @@ -161,6 +162,8 @@ void register_editor_types() { GDREGISTER_CLASS(EditorExportPlugin); GDREGISTER_ABSTRACT_CLASS(EditorExportPlatform); GDREGISTER_ABSTRACT_CLASS(EditorExportPlatformPC); + GDREGISTER_CLASS(EditorExportPlatformExtension); + GDREGISTER_ABSTRACT_CLASS(EditorExportPreset); register_exporter_types(); diff --git a/editor/rename_dialog.cpp b/editor/rename_dialog.cpp index 71d2ccf124..2ef7de1538 100644 --- a/editor/rename_dialog.cpp +++ b/editor/rename_dialog.cpp @@ -304,7 +304,7 @@ RenameDialog::RenameDialog(SceneTreeEditor *p_scene_tree_editor) { // ---- Connections - cbut_collapse_features->connect("toggled", callable_mp(this, &RenameDialog::_features_toggled)); + cbut_collapse_features->connect(SceneStringName(toggled), callable_mp(this, &RenameDialog::_features_toggled)); // Substitute Buttons diff --git a/editor/run_instances_dialog.cpp b/editor/run_instances_dialog.cpp index d617c899ad..bb32748653 100644 --- a/editor/run_instances_dialog.cpp +++ b/editor/run_instances_dialog.cpp @@ -304,7 +304,7 @@ RunInstancesDialog::RunInstancesDialog() { args_gc->add_child(instance_count); instance_count->connect(SceneStringName(value_changed), callable_mp(this, &RunInstancesDialog::_start_instance_timer).unbind(1)); instance_count->connect(SceneStringName(value_changed), callable_mp(this, &RunInstancesDialog::_refresh_argument_count).unbind(1)); - enable_multiple_instances_checkbox->connect("toggled", callable_mp(instance_count, &SpinBox::set_editable)); + enable_multiple_instances_checkbox->connect(SceneStringName(toggled), callable_mp(instance_count, &SpinBox::set_editable)); instance_count->set_editable(enable_multiple_instances_checkbox->is_pressed()); main_args_edit = memnew(LineEdit); diff --git a/editor/scene_tree_dock.cpp b/editor/scene_tree_dock.cpp index 30050901d9..3110ecb926 100644 --- a/editor/scene_tree_dock.cpp +++ b/editor/scene_tree_dock.cpp @@ -1181,7 +1181,16 @@ void SceneTreeDock::_tool_selected(int p_tool, bool p_confirm_override) { case TOOL_OPEN_DOCUMENTATION: { List<Node *> selection = editor_selection->get_selected_node_list(); for (const Node *node : selection) { - ScriptEditor::get_singleton()->goto_help("class_name:" + node->get_class()); + String class_name; + Ref<Script> script_base = node->get_script(); + if (script_base.is_valid()) { + class_name = script_base->get_global_name(); + } + if (class_name.is_empty()) { + class_name = node->get_class(); + } + + ScriptEditor::get_singleton()->goto_help("class_name:" + class_name); } EditorNode::get_singleton()->set_visible_editor(EditorNode::EDITOR_SCRIPT); } break; @@ -2641,6 +2650,13 @@ void SceneTreeDock::_delete_confirm(bool p_cut) { } } + if (!entire_scene) { + for (const Node *E : remove_list) { + // `move_child` + `get_index` doesn't really work for internal nodes. + ERR_FAIL_COND_MSG(E->get_internal_mode() != INTERNAL_MODE_DISABLED, "Trying to remove internal node, this is not supported."); + } + } + EditorNode::get_singleton()->hide_unused_editors(this); EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); @@ -2653,10 +2669,6 @@ void SceneTreeDock::_delete_confirm(bool p_cut) { undo_redo->add_undo_method(scene_tree, "update_tree"); undo_redo->add_undo_reference(edited_scene); } else { - for (const Node *E : remove_list) { - // `move_child` + `get_index` doesn't really work for internal nodes. - ERR_FAIL_COND_MSG(E->get_internal_mode() != INTERNAL_MODE_DISABLED, "Trying to remove internal node, this is not supported."); - } if (delete_tracks_checkbox->is_pressed() || p_cut) { remove_list.sort_custom<Node::Comparator>(); // Sort nodes to keep positions. HashMap<Node *, NodePath> path_renames; diff --git a/editor/script_create_dialog.cpp b/editor/script_create_dialog.cpp index 64c97e1641..f8673ddd44 100644 --- a/editor/script_create_dialog.cpp +++ b/editor/script_create_dialog.cpp @@ -100,7 +100,7 @@ static Vector<String> _get_hierarchy(const String &p_class_name) { } if (hierarchy.is_empty()) { - if (p_class_name.is_valid_identifier()) { + if (p_class_name.is_valid_ascii_identifier()) { hierarchy.push_back(p_class_name); } hierarchy.push_back("Object"); diff --git a/editor/shader_create_dialog.cpp b/editor/shader_create_dialog.cpp index fd9d5bc127..846e8867a1 100644 --- a/editor/shader_create_dialog.cpp +++ b/editor/shader_create_dialog.cpp @@ -629,7 +629,7 @@ ShaderCreateDialog::ShaderCreateDialog() { internal = memnew(CheckBox); internal->set_text(TTR("On")); - internal->connect("toggled", callable_mp(this, &ShaderCreateDialog::_built_in_toggled)); + internal->connect(SceneStringName(toggled), callable_mp(this, &ShaderCreateDialog::_built_in_toggled)); gc->add_child(memnew(Label(TTR("Built-in Shader:")))); gc->add_child(internal); diff --git a/editor/shader_globals_editor.cpp b/editor/shader_globals_editor.cpp index 0582c598a1..c05f60545d 100644 --- a/editor/shader_globals_editor.cpp +++ b/editor/shader_globals_editor.cpp @@ -349,7 +349,7 @@ String ShaderGlobalsEditor::_check_new_variable_name(const String &p_variable_na return TTR("Name cannot be empty."); } - if (!p_variable_name.is_valid_identifier()) { + if (!p_variable_name.is_valid_ascii_identifier()) { return TTR("Name must be a valid identifier."); } diff --git a/editor/themes/editor_theme_manager.cpp b/editor/themes/editor_theme_manager.cpp index 9237a62a74..f5a790353a 100644 --- a/editor/themes/editor_theme_manager.cpp +++ b/editor/themes/editor_theme_manager.cpp @@ -1471,8 +1471,44 @@ void EditorThemeManager::_populate_standard_styles(const Ref<EditorTheme> &p_the } // SpinBox. - p_theme->set_icon("updown", "SpinBox", p_theme->get_icon(SNAME("GuiSpinboxUpdown"), EditorStringName(EditorIcons))); - p_theme->set_icon("updown_disabled", "SpinBox", p_theme->get_icon(SNAME("GuiSpinboxUpdownDisabled"), EditorStringName(EditorIcons))); + { + Ref<Texture2D> empty_icon = memnew(ImageTexture); + p_theme->set_icon("updown", "SpinBox", empty_icon); + p_theme->set_icon("up", "SpinBox", p_theme->get_icon(SNAME("GuiSpinboxUp"), EditorStringName(EditorIcons))); + p_theme->set_icon("up_hover", "SpinBox", p_theme->get_icon(SNAME("GuiSpinboxUp"), EditorStringName(EditorIcons))); + p_theme->set_icon("up_pressed", "SpinBox", p_theme->get_icon(SNAME("GuiSpinboxUp"), EditorStringName(EditorIcons))); + p_theme->set_icon("up_disabled", "SpinBox", p_theme->get_icon(SNAME("GuiSpinboxUp"), EditorStringName(EditorIcons))); + p_theme->set_icon("down", "SpinBox", p_theme->get_icon(SNAME("GuiSpinboxDown"), EditorStringName(EditorIcons))); + p_theme->set_icon("down_hover", "SpinBox", p_theme->get_icon(SNAME("GuiSpinboxDown"), EditorStringName(EditorIcons))); + p_theme->set_icon("down_pressed", "SpinBox", p_theme->get_icon(SNAME("GuiSpinboxDown"), EditorStringName(EditorIcons))); + p_theme->set_icon("down_disabled", "SpinBox", p_theme->get_icon(SNAME("GuiSpinboxDown"), EditorStringName(EditorIcons))); + + p_theme->set_stylebox("up_background", "SpinBox", make_empty_stylebox()); + p_theme->set_stylebox("up_background_hovered", "SpinBox", p_config.button_style_hover); + p_theme->set_stylebox("up_background_pressed", "SpinBox", p_config.button_style_pressed); + p_theme->set_stylebox("up_background_disabled", "SpinBox", make_empty_stylebox()); + p_theme->set_stylebox("down_background", "SpinBox", make_empty_stylebox()); + p_theme->set_stylebox("down_background_hovered", "SpinBox", p_config.button_style_hover); + p_theme->set_stylebox("down_background_pressed", "SpinBox", p_config.button_style_pressed); + p_theme->set_stylebox("down_background_disabled", "SpinBox", make_empty_stylebox()); + + p_theme->set_color("up_icon_modulate", "SpinBox", p_config.font_color); + p_theme->set_color("up_hover_icon_modulate", "SpinBox", p_config.font_hover_color); + p_theme->set_color("up_pressed_icon_modulate", "SpinBox", p_config.font_pressed_color); + p_theme->set_color("up_disabled_icon_modulate", "SpinBox", p_config.font_disabled_color); + p_theme->set_color("down_icon_modulate", "SpinBox", p_config.font_color); + p_theme->set_color("down_hover_icon_modulate", "SpinBox", p_config.font_hover_color); + p_theme->set_color("down_pressed_icon_modulate", "SpinBox", p_config.font_pressed_color); + p_theme->set_color("down_disabled_icon_modulate", "SpinBox", p_config.font_disabled_color); + + p_theme->set_stylebox("field_and_buttons_separator", "SpinBox", make_empty_stylebox()); + p_theme->set_stylebox("up_down_buttons_separator", "SpinBox", make_empty_stylebox()); + + p_theme->set_constant("buttons_vertical_separation", "SpinBox", 0); + p_theme->set_constant("field_and_buttons_separation", "SpinBox", 2); + p_theme->set_constant("buttons_width", "SpinBox", 16); + p_theme->set_constant("set_min_buttons_width_from_icons", "SpinBox", 1); + } // ProgressBar. p_theme->set_stylebox("background", "ProgressBar", make_stylebox(p_theme->get_icon(SNAME("GuiProgressBar"), EditorStringName(EditorIcons)), 4, 4, 4, 4, 0, 0, 0, 0)); @@ -1768,7 +1804,9 @@ void EditorThemeManager::_populate_editor_styles(const Ref<EditorTheme> &p_theme p_theme->set_color("background", EditorStringName(Editor), background_color_opaque); p_theme->set_stylebox("Background", EditorStringName(EditorStyles), make_flat_stylebox(background_color_opaque, p_config.base_margin, p_config.base_margin, p_config.base_margin, p_config.base_margin)); - p_theme->set_stylebox("PanelForeground", EditorStringName(EditorStyles), p_config.base_style); + Ref<StyleBoxFlat> editor_panel_foreground = p_config.base_style->duplicate(); + editor_panel_foreground->set_corner_radius_all(0); + p_theme->set_stylebox("PanelForeground", EditorStringName(EditorStyles), editor_panel_foreground); // Editor focus. p_theme->set_stylebox("Focus", EditorStringName(EditorStyles), p_config.button_style_focus); @@ -1858,6 +1896,10 @@ void EditorThemeManager::_populate_editor_styles(const Ref<EditorTheme> &p_theme editor_spin_label_bg->set_border_width_all(0); p_theme->set_stylebox("label_bg", "EditorSpinSlider", editor_spin_label_bg); + // TODO Use separate arrows instead like on SpinBox. Planned for a different PR. + p_theme->set_icon("updown", "EditorSpinSlider", p_theme->get_icon(SNAME("GuiSpinboxUpdown"), EditorStringName(EditorIcons))); + p_theme->set_icon("updown_disabled", "EditorSpinSlider", p_theme->get_icon(SNAME("GuiSpinboxUpdownDisabled"), EditorStringName(EditorIcons))); + // Launch Pad and Play buttons. Ref<StyleBoxFlat> style_launch_pad = make_flat_stylebox(p_config.dark_color_1, 2 * EDSCALE, 0, 2 * EDSCALE, 0, p_config.corner_radius); style_launch_pad->set_corner_radius_all(p_config.corner_radius * EDSCALE); diff --git a/gles3_builders.py b/gles3_builders.py index a4928c81c5..a81d42b42e 100644 --- a/gles3_builders.py +++ b/gles3_builders.py @@ -3,7 +3,7 @@ import os.path from typing import Optional -from methods import print_error +from methods import print_error, to_raw_cstring class GLES3HeaderStruct: @@ -553,20 +553,12 @@ def build_gles3_header( fd.write("\t\tstatic const Feedback* _feedbacks=nullptr;\n") fd.write("\t\tstatic const char _vertex_code[]={\n") - for x in header_data.vertex_lines: - for c in x: - fd.write(str(ord(c)) + ",") - - fd.write(str(ord("\n")) + ",") - fd.write("\t\t0};\n\n") + fd.write(to_raw_cstring(header_data.vertex_lines)) + fd.write("\n\t\t};\n\n") fd.write("\t\tstatic const char _fragment_code[]={\n") - for x in header_data.fragment_lines: - for c in x: - fd.write(str(ord(c)) + ",") - - fd.write(str(ord("\n")) + ",") - fd.write("\t\t0};\n\n") + fd.write(to_raw_cstring(header_data.fragment_lines)) + fd.write("\n\t\t};\n\n") fd.write( '\t\t_setup(_vertex_code,_fragment_code,"' diff --git a/glsl_builders.py b/glsl_builders.py index 05aab3acbb..82c15fc93b 100644 --- a/glsl_builders.py +++ b/glsl_builders.py @@ -1,25 +1,9 @@ """Functions used to generate source files during build time""" import os.path -from typing import Iterable, Optional +from typing import Optional -from methods import print_error - - -def generate_inline_code(input_lines: Iterable[str], insert_newline: bool = True): - """Take header data and generate inline code - - :param: input_lines: values for shared inline code - :return: str - generated inline value - """ - output = [] - for line in input_lines: - if line: - output.append(",".join(str(ord(c)) for c in line)) - if insert_newline: - output.append("%s" % ord("\n")) - output.append("0") - return ",".join(output) +from methods import print_error, to_raw_cstring class RDHeaderStruct: @@ -127,13 +111,13 @@ def build_rd_header( if header_data.compute_lines: body_parts = [ - "static const char _compute_code[] = {\n%s\n\t\t};" % generate_inline_code(header_data.compute_lines), + "static const char _compute_code[] = {\n%s\n\t\t};" % to_raw_cstring(header_data.compute_lines), f'setup(nullptr, nullptr, _compute_code, "{out_file_class}");', ] else: body_parts = [ - "static const char _vertex_code[] = {\n%s\n\t\t};" % generate_inline_code(header_data.vertex_lines), - "static const char _fragment_code[] = {\n%s\n\t\t};" % generate_inline_code(header_data.fragment_lines), + "static const char _vertex_code[] = {\n%s\n\t\t};" % to_raw_cstring(header_data.vertex_lines), + "static const char _fragment_code[] = {\n%s\n\t\t};" % to_raw_cstring(header_data.fragment_lines), f'setup(_vertex_code, _fragment_code, nullptr, "{out_file_class}");', ] @@ -211,7 +195,7 @@ def build_raw_header( #define {out_file_ifdef}_RAW_H static const char {out_file_base}[] = {{ - {generate_inline_code(header_data.code, insert_newline=False)} +{to_raw_cstring(header_data.code)} }}; #endif """ diff --git a/godot.manifest b/godot.manifest index 30b80aff25..17b588cafd 100644 --- a/godot.manifest +++ b/godot.manifest @@ -1,5 +1,10 @@ <?xml version="1.0" encoding="utf-8"?> <assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0"> + <application xmlns="urn:schemas-microsoft-com:asm.v3"> + <windowsSettings xmlns:ws2="http://schemas.microsoft.com/SMI/2016/WindowsSettings"> + <ws2:longPathAware>true</ws2:longPathAware> + </windowsSettings> + </application> <dependency> <dependentAssembly> <assemblyIdentity diff --git a/main/main.cpp b/main/main.cpp index af8f1c692a..f2f71c27a4 100644 --- a/main/main.cpp +++ b/main/main.cpp @@ -197,6 +197,7 @@ static bool found_project = false; static bool auto_build_solutions = false; static String debug_server_uri; static bool wait_for_import = false; +static bool restore_editor_window_layout = true; #ifndef DISABLE_DEPRECATED static int converter_max_kb_file = 4 * 1024; // 4MB static int converter_max_line_length = 100000; @@ -2188,7 +2189,20 @@ Error Main::setup(const char *execpath, int argc, char *argv[], bool p_second_ph if (rendering_method != "forward_plus" && rendering_method != "mobile" && rendering_method != "gl_compatibility") { - OS::get_singleton()->print("Unknown renderer name '%s', aborting. Valid options are: %s\n", rendering_method.utf8().get_data(), renderer_hints.utf8().get_data()); + OS::get_singleton()->print("Unknown rendering method '%s', aborting.\nValid options are ", + rendering_method.utf8().get_data()); + + const Vector<String> rendering_method_hints = renderer_hints.split(","); + for (int i = 0; i < rendering_method_hints.size(); i++) { + if (i == rendering_method_hints.size() - 1) { + OS::get_singleton()->print(" and "); + } else if (i != 0) { + OS::get_singleton()->print(", "); + } + OS::get_singleton()->print("'%s'", rendering_method_hints[i].utf8().get_data()); + } + + OS::get_singleton()->print(".\n"); goto error; } } @@ -2214,12 +2228,25 @@ Error Main::setup(const char *execpath, int argc, char *argv[], bool p_second_ph OS::get_singleton()->print("Unknown rendering driver '%s', aborting.\nValid options are ", rendering_driver.utf8().get_data()); + // Deduplicate driver entries, as a rendering driver may be supported by several display servers. + Vector<String> unique_rendering_drivers; for (int i = 0; i < DisplayServer::get_create_function_count(); i++) { Vector<String> r_drivers = DisplayServer::get_create_function_rendering_drivers(i); for (int d = 0; d < r_drivers.size(); d++) { - OS::get_singleton()->print("'%s', ", r_drivers[d].utf8().get_data()); + if (!unique_rendering_drivers.has(r_drivers[d])) { + unique_rendering_drivers.append(r_drivers[d]); + } + } + } + + for (int i = 0; i < unique_rendering_drivers.size(); i++) { + if (i == unique_rendering_drivers.size() - 1) { + OS::get_singleton()->print(" and "); + } else if (i != 0) { + OS::get_singleton()->print(", "); } + OS::get_singleton()->print("'%s'", unique_rendering_drivers[i].utf8().get_data()); } OS::get_singleton()->print(".\n"); @@ -2300,15 +2327,12 @@ Error Main::setup(const char *execpath, int argc, char *argv[], bool p_second_ph } } - // note this is the desired rendering driver, it doesn't mean we will get it. - // TODO - make sure this is updated in the case of fallbacks, so that the user interface - // shows the correct driver string. - OS::get_singleton()->set_current_rendering_driver_name(rendering_driver); - OS::get_singleton()->set_current_rendering_method(rendering_method); - // always convert to lower case for consistency in the code rendering_driver = rendering_driver.to_lower(); + OS::get_singleton()->set_current_rendering_driver_name(rendering_driver); + OS::get_singleton()->set_current_rendering_method(rendering_method); + if (use_custom_res) { if (!force_res) { window_size.width = GLOBAL_GET("display/window/size/viewport_width"); @@ -2528,6 +2552,8 @@ Error Main::setup(const char *execpath, int argc, char *argv[], bool p_second_ph GLOBAL_DEF_BASIC("xr/openxr/startup_alert", true); // OpenXR project extensions settings. + GLOBAL_DEF_BASIC(PropertyInfo(Variant::INT, "xr/openxr/extensions/debug_utils", PROPERTY_HINT_ENUM, "Disabled,Error,Warning,Info,Verbose"), "0"); + GLOBAL_DEF_BASIC(PropertyInfo(Variant::INT, "xr/openxr/extensions/debug_message_types", PROPERTY_HINT_FLAGS, "General,Validation,Performance,Conformance"), "15"); GLOBAL_DEF_BASIC("xr/openxr/extensions/hand_tracking", false); GLOBAL_DEF_BASIC("xr/openxr/extensions/hand_tracking_unobstructed_data_source", false); // XR_HAND_TRACKING_DATA_SOURCE_UNOBSTRUCTED_EXT GLOBAL_DEF_BASIC("xr/openxr/extensions/hand_tracking_controller_data_source", false); // XR_HAND_TRACKING_DATA_SOURCE_CONTROLLER_EXT @@ -2692,6 +2718,7 @@ Error Main::setup2(bool p_show_boot_logo) { bool prefer_wayland_found = false; bool prefer_wayland = false; + bool remember_window_size_and_position_found = false; if (editor) { screen_property = "interface/editor/editor_screen"; @@ -2707,7 +2734,7 @@ Error Main::setup2(bool p_show_boot_logo) { prefer_wayland_found = true; } - while (!screen_found || !prefer_wayland_found) { + while (!screen_found || !prefer_wayland_found || !remember_window_size_and_position_found) { assign = Variant(); next_tag.fields.clear(); next_tag.name = String(); @@ -2727,6 +2754,11 @@ Error Main::setup2(bool p_show_boot_logo) { prefer_wayland = value; prefer_wayland_found = true; } + + if (!remember_window_size_and_position_found && assign == "interface/editor/remember_window_size_and_position") { + restore_editor_window_layout = value; + remember_window_size_and_position_found = true; + } } } @@ -2749,6 +2781,34 @@ Error Main::setup2(bool p_show_boot_logo) { } } + bool has_command_line_window_override = init_use_custom_pos || init_use_custom_screen || init_windowed; + if (editor && !has_command_line_window_override && restore_editor_window_layout) { + Ref<ConfigFile> config; + config.instantiate(); + // Load and amend existing config if it exists. + Error err = config->load(EditorPaths::get_singleton()->get_project_settings_dir().path_join("editor_layout.cfg")); + if (err == OK) { + init_screen = config->get_value("EditorWindow", "screen", init_screen); + String mode = config->get_value("EditorWindow", "mode", "maximized"); + window_size = config->get_value("EditorWindow", "size", window_size); + if (mode == "windowed") { + window_mode = DisplayServer::WINDOW_MODE_WINDOWED; + init_windowed = true; + } else if (mode == "fullscreen") { + window_mode = DisplayServer::WINDOW_MODE_FULLSCREEN; + init_fullscreen = true; + } else { + window_mode = DisplayServer::WINDOW_MODE_MAXIMIZED; + init_maximized = true; + } + + if (init_windowed) { + init_use_custom_pos = true; + init_custom_pos = config->get_value("EditorWindow", "position", Vector2i(0, 0)); + } + } + } + OS::get_singleton()->benchmark_end_measure("Startup", "Initialize Early Settings"); } #endif @@ -2893,6 +2953,30 @@ Error Main::setup2(bool p_show_boot_logo) { OS::get_singleton()->benchmark_end_measure("Servers", "Display"); } +#ifdef TOOLS_ENABLED + // If the editor is running in windowed mode, ensure the window rect fits + // the screen in case screen count or position has changed. + if (editor && init_windowed) { + // We still need to check we are actually in windowed mode, because + // certain platform might only support one fullscreen window. + if (DisplayServer::get_singleton()->window_get_mode() == DisplayServer::WINDOW_MODE_WINDOWED) { + Vector2i current_size = DisplayServer::get_singleton()->window_get_size(); + Vector2i current_pos = DisplayServer::get_singleton()->window_get_position(); + int screen = DisplayServer::get_singleton()->window_get_current_screen(); + Rect2i screen_rect = DisplayServer::get_singleton()->screen_get_usable_rect(screen); + + Vector2i adjusted_end = screen_rect.get_end().min(current_pos + current_size); + Vector2i adjusted_pos = screen_rect.get_position().max(adjusted_end - current_size); + Vector2i adjusted_size = DisplayServer::get_singleton()->window_get_min_size().max(adjusted_end - adjusted_pos); + + if (current_pos != adjusted_end || current_size != adjusted_size) { + DisplayServer::get_singleton()->window_set_position(adjusted_pos); + DisplayServer::get_singleton()->window_set_size(adjusted_size); + } + } + } +#endif + if (GLOBAL_GET("debug/settings/stdout/print_fps") || print_fps) { // Print requested V-Sync mode at startup to diagnose the printed FPS not going above the monitor refresh rate. switch (window_vsync_mode) { @@ -3045,7 +3129,7 @@ Error Main::setup2(bool p_show_boot_logo) { DisplayServer::set_early_window_clear_color_override(false); - GLOBAL_DEF_BASIC(PropertyInfo(Variant::STRING, "application/config/icon", PROPERTY_HINT_FILE, "*.png,*.webp,*.svg"), String()); + GLOBAL_DEF_BASIC(PropertyInfo(Variant::STRING, "application/config/icon", PROPERTY_HINT_FILE, "*.png,*.bmp,*.hdr,*.jpg,*.jpeg,*.svg,*.tga,*.exr,*.webp"), String()); GLOBAL_DEF(PropertyInfo(Variant::STRING, "application/config/macos_native_icon", PROPERTY_HINT_FILE, "*.icns"), String()); GLOBAL_DEF(PropertyInfo(Variant::STRING, "application/config/windows_native_icon", PROPERTY_HINT_FILE, "*.ico"), String()); @@ -3214,7 +3298,7 @@ Error Main::setup2(bool p_show_boot_logo) { OS::get_singleton()->benchmark_end_measure("Startup", "Platforms"); - GLOBAL_DEF_BASIC(PropertyInfo(Variant::STRING, "display/mouse_cursor/custom_image", PROPERTY_HINT_FILE, "*.png,*.webp"), String()); + GLOBAL_DEF_BASIC(PropertyInfo(Variant::STRING, "display/mouse_cursor/custom_image", PROPERTY_HINT_FILE, "*.png,*.bmp,*.hdr,*.jpg,*.jpeg,*.svg,*.tga,*.exr,*.webp"), String()); GLOBAL_DEF_BASIC("display/mouse_cursor/custom_image_hotspot", Vector2()); GLOBAL_DEF_BASIC("display/mouse_cursor/tooltip_position_offset", Point2(10, 10)); @@ -3307,7 +3391,8 @@ void Main::setup_boot_logo() { boot_logo.instantiate(); Error load_err = ImageLoader::load_image(boot_logo_path, boot_logo); if (load_err) { - ERR_PRINT("Non-existing or invalid boot splash at '" + boot_logo_path + "'. Loading default splash."); + String msg = (boot_logo_path.ends_with(".png") ? "" : "The only supported format is PNG."); + ERR_PRINT("Non-existing or invalid boot splash at '" + boot_logo_path + +"'. " + msg + " Loading default splash."); } } } else { @@ -3953,6 +4038,8 @@ int Main::start() { if (editor_embed_subwindows) { sml->get_root()->set_embedding_subwindows(true); } + restore_editor_window_layout = EditorSettings::get_singleton()->get_setting( + "interface/editor/remember_window_size_and_position"); } #endif diff --git a/methods.py b/methods.py index c725501fd9..bfd08cfc7b 100644 --- a/methods.py +++ b/methods.py @@ -8,7 +8,7 @@ from collections import OrderedDict from enum import Enum from io import StringIO, TextIOWrapper from pathlib import Path -from typing import Generator, Optional +from typing import Generator, List, Optional, Union # Get the "Godot" folder name ahead of time base_folder_path = str(os.path.abspath(Path(__file__).parent)) + "/" @@ -163,7 +163,7 @@ def add_source_files(self, sources, files, allow_gen=False): def disable_warnings(self): # 'self' is the environment - if self.msvc: + if self.msvc and not using_clang(self): # We have to remove existing warning level defines before appending /w, # otherwise we get: "warning D9025 : overriding '/W3' with '/w'" self["CCFLAGS"] = [x for x in self["CCFLAGS"] if not (x.startswith("/W") or x.startswith("/w"))] @@ -495,6 +495,8 @@ def use_windows_spawn_fix(self, platform=None): rv = proc.wait() if rv: print_error(err) + elif len(err) > 0 and not err.isspace(): + print(err) return rv def mySpawn(sh, escape, cmd, args, env): @@ -815,21 +817,20 @@ def get_compiler_version(env): "apple_patch3": -1, } - if not env.msvc: - # Not using -dumpversion as some GCC distros only return major, and - # Clang used to return hardcoded 4.2.1: # https://reviews.llvm.org/D56803 - try: - version = ( - subprocess.check_output([env.subst(env["CXX"]), "--version"], shell=(os.name == "nt")) - .strip() - .decode("utf-8") - ) - except (subprocess.CalledProcessError, OSError): - print_warning("Couldn't parse CXX environment variable to infer compiler version.") - return ret - else: + if env.msvc and not using_clang(env): # TODO: Implement for MSVC return ret + + # Not using -dumpversion as some GCC distros only return major, and + # Clang used to return hardcoded 4.2.1: # https://reviews.llvm.org/D56803 + try: + version = subprocess.check_output( + [env.subst(env["CXX"]), "--version"], shell=(os.name == "nt"), encoding="utf-8" + ).strip() + except (subprocess.CalledProcessError, OSError): + print_warning("Couldn't parse CXX environment variable to infer compiler version.") + return ret + match = re.search( r"(?:(?<=version )|(?<=\) )|(?<=^))" r"(?P<major>\d+)" @@ -1641,3 +1642,43 @@ def generated_wrapper( file.write(f"\n\n#endif // {header_guard}") file.write("\n") + + +def to_raw_cstring(value: Union[str, List[str]]) -> str: + MAX_LITERAL = 16 * 1024 + + if isinstance(value, list): + value = "\n".join(value) + "\n" + + split: List[bytes] = [] + offset = 0 + encoded = value.encode() + + while offset <= len(encoded): + segment = encoded[offset : offset + MAX_LITERAL] + offset += MAX_LITERAL + if len(segment) == MAX_LITERAL: + # Try to segment raw strings at double newlines to keep readable. + pretty_break = segment.rfind(b"\n\n") + if pretty_break != -1: + segment = segment[: pretty_break + 1] + offset -= MAX_LITERAL - pretty_break - 1 + # If none found, ensure we end with valid utf8. + # https://github.com/halloleo/unicut/blob/master/truncate.py + elif segment[-1] & 0b10000000: + last_11xxxxxx_index = [i for i in range(-1, -5, -1) if segment[i] & 0b11000000 == 0b11000000][0] + last_11xxxxxx = segment[last_11xxxxxx_index] + if not last_11xxxxxx & 0b00100000: + last_char_length = 2 + elif not last_11xxxxxx & 0b0010000: + last_char_length = 3 + elif not last_11xxxxxx & 0b0001000: + last_char_length = 4 + + if last_char_length > -last_11xxxxxx_index: + segment = segment[:last_11xxxxxx_index] + offset += last_11xxxxxx_index + + split += [segment] + + return " ".join(f'R"<!>({x.decode()})<!>"' for x in split) diff --git a/misc/dist/linux/org.godotengine.Godot.desktop b/misc/dist/linux/org.godotengine.Godot.desktop index d351839205..28669548d6 100644 --- a/misc/dist/linux/org.godotengine.Godot.desktop +++ b/misc/dist/linux/org.godotengine.Godot.desktop @@ -4,11 +4,13 @@ GenericName=Libre game engine GenericName[el]=Ελεύθερη μηχανή παιχνιδιού GenericName[fr]=Moteur de jeu libre GenericName[nl]=Libre game-engine +GenericName[ru]=Свободный игровой движок GenericName[zh_CN]=自由的游戏引擎 Comment=Multi-platform 2D and 3D game engine with a feature-rich editor Comment[el]=2D και 3D μηχανή παιχνιδιού πολλαπλών πλατφορμών με επεξεργαστή πλούσιο σε χαρακτηριστικά Comment[fr]=Moteur de jeu 2D et 3D multiplateforme avec un éditeur riche en fonctionnalités -Comment[nl]=Multi-platform 2D- en 3d-game-engine met een veelzijdige editor +Comment[nl]=Multi-platform 2D- en 3D-game-engine met een veelzijdige editor +Comment[ru]=Кроссплатформенный движок с многофункциональным редактором для 2D- и 3D-игр Comment[zh_CN]=多平台 2D 和 3D 游戏引擎,带有功能丰富的编辑器 Exec=godot %f Icon=godot diff --git a/misc/extension_api_validation/4.3-stable.expected b/misc/extension_api_validation/4.3-stable.expected index 733d85c46e..882f96a0dc 100644 --- a/misc/extension_api_validation/4.3-stable.expected +++ b/misc/extension_api_validation/4.3-stable.expected @@ -26,3 +26,50 @@ Validate extension JSON: Error: Field 'classes/RenderingDevice/methods/draw_list draw_list_begin added a new optional debug argument called breadcrumb. There used to be an Array argument as arg #9 initially, then changed to typedarray::RID in 4.1, and finally removed in 4.3. Since we're adding a new one at the same location, we need to silence those warnings for 4.1 and 4.3. + + +GH-95126 +-------- +Validate extension JSON: Error: Field 'classes/Shader/methods/get_default_texture_parameter/return_value': type changed value in new API, from "Texture2D" to "Texture". +Validate extension JSON: Error: Field 'classes/Shader/methods/set_default_texture_parameter/arguments/1': type changed value in new API, from "Texture2D" to "Texture". +Validate extension JSON: Error: Field 'classes/VisualShaderNodeCubemap/methods/get_cube_map/return_value': type changed value in new API, from "Cubemap" to "TextureLayered". +Validate extension JSON: Error: Field 'classes/VisualShaderNodeCubemap/methods/set_cube_map/arguments/0': type changed value in new API, from "Cubemap" to "TextureLayered". +Validate extension JSON: Error: Field 'classes/VisualShaderNodeCubemap/properties/cube_map': type changed value in new API, from "Cubemap" to "Cubemap,CompressedCubemap,PlaceholderCubemap,TextureCubemapRD". +Validate extension JSON: Error: Field 'classes/VisualShaderNodeTexture2DArray/methods/get_texture_array/return_value': type changed value in new API, from "Texture2DArray" to "TextureLayered". +Validate extension JSON: Error: Field 'classes/VisualShaderNodeTexture2DArray/methods/set_texture_array/arguments/0': type changed value in new API, from "Texture2DArray" to "TextureLayered". +Validate extension JSON: Error: Field 'classes/VisualShaderNodeTexture2DArray/properties/texture_array': type changed value in new API, from "Texture2DArray" to "Texture2DArray,CompressedTexture2DArray,PlaceholderTexture2DArray,Texture2DArrayRD". + +Allow setting a cubemap as default parameter to shader. +Compatibility methods registered. + + +GH-93605 +-------- +Validate extension JSON: JSON file: Field was added in a way that breaks compatibility 'classes/Semaphore/methods/post': arguments + +Optional arguments added. Compatibility methods registered. + + +GH-95212 +-------- +Validate extension JSON: Error: Field 'classes/RegEx/methods/compile/arguments': size changed value in new API, from 1 to 2. +Validate extension JSON: Error: Field 'classes/RegEx/methods/create_from_string/arguments': size changed value in new API, from 1 to 2. + +Add optional argument to control error printing on compilation fail. Compatibility methods registered. + + +GH-95375 +-------- +Validate extension JSON: Error: Field 'classes/AudioStreamPlayer/properties/playing': setter changed value in new API, from "_set_playing" to &"set_playing". +Validate extension JSON: Error: Field 'classes/AudioStreamPlayer2D/properties/playing': setter changed value in new API, from "_set_playing" to &"set_playing". +Validate extension JSON: Error: Field 'classes/AudioStreamPlayer3D/properties/playing': setter changed value in new API, from "_set_playing" to &"set_playing". + +These setters have been renamed to expose them. GDExtension language bindings couldn't have exposed these properties before. + + +GH-94322 +-------- +Validate extension JSON: Error: Field 'classes/EditorInterface/methods/popup_node_selector/arguments': size changed value in new API, from 2 to 3. +Validate extension JSON: Error: Field 'classes/EditorInterface/methods/popup_property_selector/arguments': size changed value in new API, from 3 to 4. + +Added optional argument to popup_property_selector and popup_node_selector to specify the current value. diff --git a/modules/betsy/bc6h.glsl b/modules/betsy/bc6h.glsl index 0d10d378fd..37e7591aea 100644 --- a/modules/betsy/bc6h.glsl +++ b/modules/betsy/bc6h.glsl @@ -1,7 +1,7 @@ #[versions] signed = "#define SIGNED"; -unsigned = ""; +unsigned = "#define QUALITY"; // The "Quality" preset causes artifacting on signed data, so for now it's exclusive to unsigned. #[compute] #version 450 @@ -10,10 +10,6 @@ unsigned = ""; #include "UavCrossPlatform_piece_all.glsl" #VERSION_DEFINES -#define QUALITY - -//SIGNED macro is WIP -//#define SIGNED float3 f32tof16(float3 value) { return float3(packHalf2x16(float2(value.x, 0.0)), @@ -48,11 +44,59 @@ params; const float HALF_MAX = 65504.0f; const uint PATTERN_NUM = 32u; +#ifdef SIGNED +const float HALF_MIN = -65504.0f; +#else +const float HALF_MIN = 0.0f; +#endif + +#ifdef SIGNED +// https://github.com/godotengine/godot/pull/96377#issuecomment-2323488254 +// https://github.com/godotengine/godot/pull/96377#issuecomment-2323450950 +bool isNegative(float a) { + return a < 0.0f; +} + +float CalcSignlessMSLE(float a, float b) { + float err = log2((b + 1.0f) / (a + 1.0f)); + err = err * err; + return err; +} + +float CrossCalcMSLE(float a, float b) { + float result = 0.0f; + result += CalcSignlessMSLE(0.0f, abs(a)); + result += CalcSignlessMSLE(0.0f, abs(b)); + return result; +} + +float CalcMSLE(float3 a, float3 b) { + float result = 0.0f; + if (isNegative(a.x) != isNegative(b.x)) { + result += CrossCalcMSLE(a.x, b.x); + } else { + result += CalcSignlessMSLE(abs(a.x), abs(b.x)); + } + if (isNegative(a.y) != isNegative(b.y)) { + result += CrossCalcMSLE(a.y, b.y); + } else { + result += CalcSignlessMSLE(abs(a.y), abs(b.y)); + } + if (isNegative(a.z) != isNegative(b.z)) { + result += CrossCalcMSLE(a.z, b.z); + } else { + result += CalcSignlessMSLE(abs(a.z), abs(b.z)); + } + + return result; +} +#else float CalcMSLE(float3 a, float3 b) { float3 err = log2((b + 1.0f) / (a + 1.0f)); err = err * err; return err.x + err.y + err.z; } +#endif uint PatternFixupID(uint i) { uint ret = 15u; @@ -176,11 +220,6 @@ float3 Unquantize10(float3 x) { float3 FinishUnquantize(float3 endpoint0Unq, float3 endpoint1Unq, float weight) { float3 comp = (endpoint0Unq * (64.0f - weight) + endpoint1Unq * weight + 32.0f) * (31.0f / 2048.0f); - /*float3 signVal; - signVal.x = comp.x >= 0.0f ? 0.0f : 0x8000; - signVal.y = comp.y >= 0.0f ? 0.0f : 0x8000; - signVal.z = comp.z >= 0.0f ? 0.0f : 0x8000;*/ - //return f16tof32( uint3( signVal + abs( comp ) ) ); return f16tof32(uint3(comp)); } #endif @@ -207,6 +246,7 @@ uint ComputeIndex4(float texelPos, float endPoint0Pos, float endPoint1Pos) { return uint(clamp(r * 14.93333f + 0.03333f + 0.5f, 0.0f, 15.0f)); } +// This adds a bitflag to quantized values that signifies whether they are negative. void SignExtend(inout float3 v1, uint mask, uint signFlag) { int3 v = int3(v1); v.x = (v.x & int(mask)) | (v.x < 0 ? int(signFlag) : 0); @@ -215,6 +255,7 @@ void SignExtend(inout float3 v1, uint mask, uint signFlag) { v1 = v; } +// Encodes a block with mode 11 (2x 10-bit endpoints). void EncodeP1(inout uint4 block, inout float blockMSLE, float3 texels[16]) { // compute endpoints (min/max RGB bbox) float3 blockMin = texels[0]; @@ -250,6 +291,12 @@ void EncodeP1(inout uint4 block, inout float blockMSLE, float3 texels[16]) { float endPoint0Pos = f32tof16(dot(blockMin, blockDir)); float endPoint1Pos = f32tof16(dot(blockMax, blockDir)); +#ifdef SIGNED + int maxVal10 = 0x1FF; + endpoint0 = clamp(endpoint0, -maxVal10, maxVal10); + endpoint1 = clamp(endpoint1, -maxVal10, maxVal10); +#endif + // check if endpoint swap is required float fixupTexelPos = f32tof16(dot(texels[0], blockDir)); uint fixupIndex = ComputeIndex4(fixupTexelPos, endPoint0Pos, endPoint1Pos); @@ -276,6 +323,11 @@ void EncodeP1(inout uint4 block, inout float blockMSLE, float3 texels[16]) { msle += CalcMSLE(texels[i], texelUnc); } +#ifdef SIGNED + SignExtend(endpoint0, 0x1FF, 0x200); + SignExtend(endpoint1, 0x1FF, 0x200); +#endif + // encode block for mode 11 blockMSLE = msle; block.x = 0x03; @@ -316,11 +368,12 @@ float DistToLineSq(float3 PointOnLine, float3 LineDirection, float3 Point) { return dot(x, x); } +// Gets the deviation from the source data of a particular pattern (smaller is better). float EvaluateP2Pattern(uint pattern, float3 texels[16]) { float3 p0BlockMin = float3(HALF_MAX, HALF_MAX, HALF_MAX); - float3 p0BlockMax = float3(0.0f, 0.0f, 0.0f); + float3 p0BlockMax = float3(HALF_MIN, HALF_MIN, HALF_MIN); float3 p1BlockMin = float3(HALF_MAX, HALF_MAX, HALF_MAX); - float3 p1BlockMax = float3(0.0f, 0.0f, 0.0f); + float3 p1BlockMax = float3(HALF_MIN, HALF_MIN, HALF_MIN); for (uint i = 0; i < 16; ++i) { uint paletteID = Pattern(pattern, i); @@ -350,11 +403,12 @@ float EvaluateP2Pattern(uint pattern, float3 texels[16]) { return sqDistanceFromLine; } +// Encodes a block with either mode 2 (7-bit base, 3x 6-bit delta), or mode 6 (9-bit base, 3x 5-bit delta). Both use pattern encoding. void EncodeP2Pattern(inout uint4 block, inout float blockMSLE, uint pattern, float3 texels[16]) { float3 p0BlockMin = float3(HALF_MAX, HALF_MAX, HALF_MAX); - float3 p0BlockMax = float3(0.0f, 0.0f, 0.0f); + float3 p0BlockMax = float3(HALF_MIN, HALF_MIN, HALF_MIN); float3 p1BlockMin = float3(HALF_MAX, HALF_MAX, HALF_MAX); - float3 p1BlockMax = float3(0.0f, 0.0f, 0.0f); + float3 p1BlockMax = float3(HALF_MIN, HALF_MIN, HALF_MIN); for (uint i = 0u; i < 16u; ++i) { uint paletteID = Pattern(pattern, i); @@ -430,6 +484,13 @@ void EncodeP2Pattern(inout uint4 block, inout float blockMSLE, uint pattern, flo endpoint952 = clamp(endpoint952, -maxVal95, maxVal95); endpoint953 = clamp(endpoint953, -maxVal95, maxVal95); +#ifdef SIGNED + int maxVal7 = 0x3F; + int maxVal9 = 0xFF; + endpoint760 = clamp(endpoint760, -maxVal7, maxVal7); + endpoint950 = clamp(endpoint950, -maxVal9, maxVal9); +#endif + float3 endpoint760Unq = Unquantize7(endpoint760); float3 endpoint761Unq = Unquantize7(endpoint760 + endpoint761); float3 endpoint762Unq = Unquantize7(endpoint760 + endpoint762); @@ -465,6 +526,11 @@ void EncodeP2Pattern(inout uint4 block, inout float blockMSLE, uint pattern, flo SignExtend(endpoint952, 0xF, 0x10); SignExtend(endpoint953, 0xF, 0x10); +#ifdef SIGNED + SignExtend(endpoint760, 0x3F, 0x40); + SignExtend(endpoint950, 0xFF, 0x100); +#endif + // encode block float p2MSLE = min(msle76, msle95); if (p2MSLE < blockMSLE) { @@ -637,7 +703,7 @@ void main() { float bestScore = EvaluateP2Pattern(0, texels); uint bestPattern = 0; - for (uint i = 1u; i < 32u; ++i) { + for (uint i = 1u; i < PATTERN_NUM; ++i) { float score = EvaluateP2Pattern(i, texels); if (score < bestScore) { diff --git a/modules/betsy/image_compress_betsy.cpp b/modules/betsy/image_compress_betsy.cpp index 6a0862e729..7f723826d1 100644 --- a/modules/betsy/image_compress_betsy.cpp +++ b/modules/betsy/image_compress_betsy.cpp @@ -49,31 +49,6 @@ static int get_next_multiple(int n, int m) { return n + (m - (n % m)); } -static bool is_image_signed(const Image *r_img) { - if (r_img->get_format() >= Image::FORMAT_RH && r_img->get_format() <= Image::FORMAT_RGBAH) { - const uint16_t *img_data = reinterpret_cast<const uint16_t *>(r_img->ptr()); - const uint64_t img_size = r_img->get_data_size() / 2; - - for (uint64_t i = 0; i < img_size; i++) { - if ((img_data[i] & 0x8000) != 0 && (img_data[i] & 0x7fff) != 0) { - return true; - } - } - - } else if (r_img->get_format() >= Image::FORMAT_RF && r_img->get_format() <= Image::FORMAT_RGBAF) { - const uint32_t *img_data = reinterpret_cast<const uint32_t *>(r_img->ptr()); - const uint64_t img_size = r_img->get_data_size() / 4; - - for (uint64_t i = 0; i < img_size; i++) { - if ((img_data[i] & 0x80000000) != 0 && (img_data[i] & 0x7fffffff) != 0) { - return true; - } - } - } - - return false; -} - Error _compress_betsy(BetsyFormat p_format, Image *r_img) { uint64_t start_time = OS::get_singleton()->get_ticks_msec(); @@ -125,7 +100,7 @@ Error _compress_betsy(BetsyFormat p_format, Image *r_img) { case BETSY_FORMAT_BC6: { err = compute_shader->parse_versions_from_text(bc6h_shader_glsl); - if (is_image_signed(r_img)) { + if (r_img->detect_signed(true)) { dest_format = Image::FORMAT_BPTC_RGBF; version = "signed"; } else { diff --git a/modules/cvtt/image_compress_cvtt.cpp b/modules/cvtt/image_compress_cvtt.cpp index 6b905c5ea1..2087dde2a1 100644 --- a/modules/cvtt/image_compress_cvtt.cpp +++ b/modules/cvtt/image_compress_cvtt.cpp @@ -174,17 +174,7 @@ void image_compress_cvtt(Image *p_image, Image::UsedChannels p_channels) { p_image->convert(Image::FORMAT_RGBH); } - const uint8_t *rb = p_image->get_data().ptr(); - - const uint16_t *source_data = reinterpret_cast<const uint16_t *>(&rb[0]); - int pixel_element_count = w * h * 3; - for (int i = 0; i < pixel_element_count; i++) { - if ((source_data[i] & 0x8000) != 0 && (source_data[i] & 0x7fff) != 0) { - is_signed = true; - break; - } - } - + is_signed = p_image->detect_signed(); target_format = is_signed ? Image::FORMAT_BPTC_RGBF : Image::FORMAT_BPTC_RGBFU; } else { p_image->convert(Image::FORMAT_RGBA8); //still uses RGBA to convert diff --git a/modules/fbx/editor/editor_scene_importer_fbx2gltf.cpp b/modules/fbx/editor/editor_scene_importer_fbx2gltf.cpp index daa4db37df..f5b19f803a 100644 --- a/modules/fbx/editor/editor_scene_importer_fbx2gltf.cpp +++ b/modules/fbx/editor/editor_scene_importer_fbx2gltf.cpp @@ -124,7 +124,7 @@ Node *EditorSceneFormatImporterFBX2GLTF::import_scene(const String &p_path, uint #endif } -Variant EditorSceneFormatImporterFBX2GLTF::get_option_visibility(const String &p_path, bool p_for_animation, +Variant EditorSceneFormatImporterFBX2GLTF::get_option_visibility(const String &p_path, const String &p_scene_import_type, const String &p_option, const HashMap<StringName, Variant> &p_options) { // Remove all the FBX options except for 'fbx/importer' if the importer is fbx2gltf. // These options are available only for ufbx. diff --git a/modules/fbx/editor/editor_scene_importer_fbx2gltf.h b/modules/fbx/editor/editor_scene_importer_fbx2gltf.h index c68e37f0d8..ce2bac6fcf 100644 --- a/modules/fbx/editor/editor_scene_importer_fbx2gltf.h +++ b/modules/fbx/editor/editor_scene_importer_fbx2gltf.h @@ -49,7 +49,7 @@ public: List<String> *r_missing_deps, Error *r_err = nullptr) override; virtual void get_import_options(const String &p_path, List<ResourceImporter::ImportOption> *r_options) override; - virtual Variant get_option_visibility(const String &p_path, bool p_for_animation, const String &p_option, + virtual Variant get_option_visibility(const String &p_path, const String &p_scene_import_type, const String &p_option, const HashMap<StringName, Variant> &p_options) override; virtual void handle_compatibility_options(HashMap<StringName, Variant> &p_import_params) const override; }; diff --git a/modules/fbx/editor/editor_scene_importer_ufbx.cpp b/modules/fbx/editor/editor_scene_importer_ufbx.cpp index 4d5f220539..64075c0664 100644 --- a/modules/fbx/editor/editor_scene_importer_ufbx.cpp +++ b/modules/fbx/editor/editor_scene_importer_ufbx.cpp @@ -88,7 +88,7 @@ Node *EditorSceneFormatImporterUFBX::import_scene(const String &p_path, uint32_t return fbx->generate_scene(state, state->get_bake_fps(), (bool)p_options["animation/trimming"], false); } -Variant EditorSceneFormatImporterUFBX::get_option_visibility(const String &p_path, bool p_for_animation, +Variant EditorSceneFormatImporterUFBX::get_option_visibility(const String &p_path, const String &p_scene_import_type, const String &p_option, const HashMap<StringName, Variant> &p_options) { return true; } diff --git a/modules/fbx/editor/editor_scene_importer_ufbx.h b/modules/fbx/editor/editor_scene_importer_ufbx.h index b81b8df4c1..6e3eafc100 100644 --- a/modules/fbx/editor/editor_scene_importer_ufbx.h +++ b/modules/fbx/editor/editor_scene_importer_ufbx.h @@ -53,7 +53,7 @@ public: List<String> *r_missing_deps, Error *r_err = nullptr) override; virtual void get_import_options(const String &p_path, List<ResourceImporter::ImportOption> *r_options) override; - virtual Variant get_option_visibility(const String &p_path, bool p_for_animation, const String &p_option, + virtual Variant get_option_visibility(const String &p_path, const String &p_scene_import_type, const String &p_option, const HashMap<StringName, Variant> &p_options) override; virtual void handle_compatibility_options(HashMap<StringName, Variant> &p_import_params) const override; }; diff --git a/modules/fbx/fbx_document.cpp b/modules/fbx/fbx_document.cpp index 4d3f7554c0..4e1a00cad6 100644 --- a/modules/fbx/fbx_document.cpp +++ b/modules/fbx/fbx_document.cpp @@ -2017,6 +2017,7 @@ void FBXDocument::_process_mesh_instances(Ref<FBXState> p_state, Node *p_scene_r ERR_CONTINUE_MSG(skeleton == nullptr, vformat("Unable to find Skeleton for node %d skin %d", node_i, skin_i)); mi->get_parent()->remove_child(mi); + mi->set_owner(nullptr); skeleton->add_child(mi, true); mi->set_owner(skeleton->get_owner()); diff --git a/modules/gdscript/doc_classes/@GDScript.xml b/modules/gdscript/doc_classes/@GDScript.xml index 104fc15a8c..f539f27848 100644 --- a/modules/gdscript/doc_classes/@GDScript.xml +++ b/modules/gdscript/doc_classes/@GDScript.xml @@ -133,7 +133,7 @@ - An [Object]-derived class which exists in [ClassDB], for example [Node]. - A [Script] (you can use any class, including inner one). Unlike the right operand of the [code]is[/code] operator, [param type] can be a non-constant value. The [code]is[/code] operator supports more features (such as typed arrays). Use the operator instead of this method if you do not need dynamic type checking. - Examples: + [b]Examples:[/b] [codeblock] print(is_instance_of(a, TYPE_INT)) print(is_instance_of(a, Node)) @@ -220,7 +220,7 @@ [code]range(b: int, n: int, s: int)[/code]: Starts from [code]b[/code], increases/decreases by steps of [code]s[/code], and stops [i]before[/i] [code]n[/code]. The arguments [code]b[/code] and [code]n[/code] are [b]inclusive[/b] and [b]exclusive[/b], respectively. The argument [code]s[/code] [b]can[/b] be negative, but not [code]0[/code]. If [code]s[/code] is [code]0[/code], an error message is printed. [method range] converts all arguments to [int] before processing. [b]Note:[/b] Returns an empty array if no value meets the value constraint (e.g. [code]range(2, 5, -1)[/code] or [code]range(5, 5, 1)[/code]). - Examples: + [b]Examples:[/b] [codeblock] print(range(4)) # Prints [0, 1, 2, 3] print(range(2, 5)) # Prints [2, 3, 4] diff --git a/modules/gdscript/gdscript.cpp b/modules/gdscript/gdscript.cpp index 7bf5e946fb..e3f2a61090 100644 --- a/modules/gdscript/gdscript.cpp +++ b/modules/gdscript/gdscript.cpp @@ -53,9 +53,11 @@ #include "core/io/file_access_encrypted.h" #include "core/os/os.h" +#include "scene/resources/packed_scene.h" #include "scene/scene_string_names.h" #ifdef TOOLS_ENABLED +#include "core/extension/gdextension_manager.h" #include "editor/editor_paths.h" #endif @@ -952,7 +954,8 @@ bool GDScript::_get(const StringName &p_name, Variant &r_ret) const { if (E) { if (likely(top->valid) && E->value.getter) { Callable::CallError ce; - r_ret = const_cast<GDScript *>(this)->callp(E->value.getter, nullptr, 0, ce); + const Variant ret = const_cast<GDScript *>(this)->callp(E->value.getter, nullptr, 0, ce); + r_ret = (ce.error == Callable::CallError::CALL_OK) ? ret : Variant(); return true; } r_ret = top->static_variables[E->value.index]; @@ -1725,10 +1728,9 @@ bool GDScriptInstance::get(const StringName &p_name, Variant &r_ret) const { if (E) { if (likely(script->valid) && E->value.getter) { Callable::CallError err; - r_ret = const_cast<GDScriptInstance *>(this)->callp(E->value.getter, nullptr, 0, err); - if (err.error == Callable::CallError::CALL_OK) { - return true; - } + const Variant ret = const_cast<GDScriptInstance *>(this)->callp(E->value.getter, nullptr, 0, err); + r_ret = (err.error == Callable::CallError::CALL_OK) ? ret : Variant(); + return true; } r_ret = members[E->value.index]; return true; @@ -1750,7 +1752,8 @@ bool GDScriptInstance::get(const StringName &p_name, Variant &r_ret) const { if (E) { if (likely(sptr->valid) && E->value.getter) { Callable::CallError ce; - r_ret = const_cast<GDScript *>(sptr)->callp(E->value.getter, nullptr, 0, ce); + const Variant ret = const_cast<GDScript *>(sptr)->callp(E->value.getter, nullptr, 0, ce); + r_ret = (ce.error == Callable::CallError::CALL_OK) ? ret : Variant(); return true; } r_ret = sptr->static_variables[E->value.index]; @@ -2176,9 +2179,26 @@ void GDScriptLanguage::_add_global(const StringName &p_name, const Variant &p_va global_array.write[globals[p_name]] = p_value; return; } - globals[p_name] = global_array.size(); - global_array.push_back(p_value); - _global_array = global_array.ptrw(); + + if (global_array_empty_indexes.size()) { + int index = global_array_empty_indexes[global_array_empty_indexes.size() - 1]; + globals[p_name] = index; + global_array.write[index] = p_value; + global_array_empty_indexes.resize(global_array_empty_indexes.size() - 1); + } else { + globals[p_name] = global_array.size(); + global_array.push_back(p_value); + _global_array = global_array.ptrw(); + } +} + +void GDScriptLanguage::_remove_global(const StringName &p_name) { + if (!globals.has(p_name)) { + return; + } + global_array_empty_indexes.push_back(globals[p_name]); + global_array.write[globals[p_name]] = Variant::NIL; + globals.erase(p_name); } void GDScriptLanguage::add_global_constant(const StringName &p_variable, const Variant &p_value) { @@ -2236,11 +2256,40 @@ void GDScriptLanguage::init() { _add_global(E.name, E.ptr); } +#ifdef TOOLS_ENABLED + if (Engine::get_singleton()->is_editor_hint()) { + GDExtensionManager::get_singleton()->connect("extension_loaded", callable_mp(this, &GDScriptLanguage::_extension_loaded)); + GDExtensionManager::get_singleton()->connect("extension_unloading", callable_mp(this, &GDScriptLanguage::_extension_unloading)); + } +#endif + #ifdef TESTS_ENABLED GDScriptTests::GDScriptTestRunner::handle_cmdline(); #endif } +#ifdef TOOLS_ENABLED +void GDScriptLanguage::_extension_loaded(const Ref<GDExtension> &p_extension) { + List<StringName> class_list; + ClassDB::get_extension_class_list(p_extension, &class_list); + for (const StringName &n : class_list) { + if (globals.has(n)) { + continue; + } + Ref<GDScriptNativeClass> nc = memnew(GDScriptNativeClass(n)); + _add_global(n, nc); + } +} + +void GDScriptLanguage::_extension_unloading(const Ref<GDExtension> &p_extension) { + List<StringName> class_list; + ClassDB::get_extension_class_list(p_extension, &class_list); + for (const StringName &n : class_list) { + _remove_global(n); + } +} +#endif + String GDScriptLanguage::get_type() const { return "GDScript"; } @@ -2503,7 +2552,7 @@ void GDScriptLanguage::reload_scripts(const Array &p_scripts, bool p_soft_reload SelfList<GDScript> *elem = script_list.first(); while (elem) { // Scripts will reload all subclasses, so only reload root scripts. - if (elem->self()->is_root_script() && elem->self()->get_path().is_resource_file()) { + if (elem->self()->is_root_script() && !elem->self()->get_path().is_empty()) { scripts.push_back(Ref<GDScript>(elem->self())); //cast to gdscript to avoid being erased by accident } elem = elem->next(); @@ -2571,7 +2620,19 @@ void GDScriptLanguage::reload_scripts(const Array &p_scripts, bool p_soft_reload for (KeyValue<Ref<GDScript>, HashMap<ObjectID, List<Pair<StringName, Variant>>>> &E : to_reload) { Ref<GDScript> scr = E.key; print_verbose("GDScript: Reloading: " + scr->get_path()); - scr->load_source_code(scr->get_path()); + if (scr->is_built_in()) { + // TODO: It would be nice to do it more efficiently than loading the whole scene again. + Ref<PackedScene> scene = ResourceLoader::load(scr->get_path().get_slice("::", 0), "", ResourceFormatLoader::CACHE_MODE_IGNORE_DEEP); + ERR_CONTINUE(scene.is_null()); + + Ref<SceneState> state = scene->get_state(); + Ref<GDScript> fresh = state->get_sub_resource(scr->get_path()); + ERR_CONTINUE(fresh.is_null()); + + scr->set_source_code(fresh->get_source_code()); + } else { + scr->load_source_code(scr->get_path()); + } scr->reload(p_soft_reload); //restore state if saved diff --git a/modules/gdscript/gdscript.h b/modules/gdscript/gdscript.h index 6527a0ea4d..4d21651365 100644 --- a/modules/gdscript/gdscript.h +++ b/modules/gdscript/gdscript.h @@ -417,6 +417,7 @@ class GDScriptLanguage : public ScriptLanguage { Vector<Variant> global_array; HashMap<StringName, int> globals; HashMap<StringName, Variant> named_globals; + Vector<int> global_array_empty_indexes; struct CallLevel { Variant *stack = nullptr; @@ -448,6 +449,7 @@ class GDScriptLanguage : public ScriptLanguage { int _debug_max_call_stack = 0; void _add_global(const StringName &p_name, const Variant &p_value); + void _remove_global(const StringName &p_name); friend class GDScriptInstance; @@ -467,6 +469,11 @@ class GDScriptLanguage : public ScriptLanguage { HashMap<String, ObjectID> orphan_subclasses; +#ifdef TOOLS_ENABLED + void _extension_loaded(const Ref<GDExtension> &p_extension); + void _extension_unloading(const Ref<GDExtension> &p_extension); +#endif + public: int calls; diff --git a/modules/gdscript/gdscript_analyzer.cpp b/modules/gdscript/gdscript_analyzer.cpp index e98cae765b..7e29a9c0fe 100644 --- a/modules/gdscript/gdscript_analyzer.cpp +++ b/modules/gdscript/gdscript_analyzer.cpp @@ -418,6 +418,12 @@ Error GDScriptAnalyzer::resolve_class_inheritance(GDScriptParser::ClassNode *p_c return err; } +#ifdef DEBUG_ENABLED + if (!parser->_is_tool && ext_parser->get_parser()->_is_tool) { + parser->push_warning(p_class, GDScriptWarning::MISSING_TOOL); + } +#endif + base = ext_parser->get_parser()->head->get_datatype(); } else { if (p_class->extends.is_empty()) { @@ -445,6 +451,13 @@ Error GDScriptAnalyzer::resolve_class_inheritance(GDScriptParser::ClassNode *p_c push_error(vformat(R"(Could not resolve super class inheritance from "%s".)", name), id); return err; } + +#ifdef DEBUG_ENABLED + if (!parser->_is_tool && base_parser->get_parser()->_is_tool) { + parser->push_warning(p_class, GDScriptWarning::MISSING_TOOL); + } +#endif + base = base_parser->get_parser()->head->get_datatype(); } } else if (ProjectSettings::get_singleton()->has_autoload(name) && ProjectSettings::get_singleton()->get_autoload(name).is_singleton) { @@ -465,6 +478,13 @@ Error GDScriptAnalyzer::resolve_class_inheritance(GDScriptParser::ClassNode *p_c push_error(vformat(R"(Could not resolve super class inheritance from "%s".)", name), id); return err; } + +#ifdef DEBUG_ENABLED + if (!parser->_is_tool && info_parser->get_parser()->_is_tool) { + parser->push_warning(p_class, GDScriptWarning::MISSING_TOOL); + } +#endif + base = info_parser->get_parser()->head->get_datatype(); } else if (class_exists(name)) { if (Engine::get_singleton()->has_singleton(name)) { diff --git a/modules/gdscript/gdscript_cache.cpp b/modules/gdscript/gdscript_cache.cpp index 3b6526ffd9..b3c0744bdf 100644 --- a/modules/gdscript/gdscript_cache.cpp +++ b/modules/gdscript/gdscript_cache.cpp @@ -144,6 +144,14 @@ GDScriptParserRef::~GDScriptParserRef() { GDScriptCache *GDScriptCache::singleton = nullptr; +SafeBinaryMutex<GDScriptCache::BINARY_MUTEX_TAG> &_get_gdscript_cache_mutex() { + return GDScriptCache::mutex; +} + +template <> +thread_local SafeBinaryMutex<GDScriptCache::BINARY_MUTEX_TAG>::TLSData SafeBinaryMutex<GDScriptCache::BINARY_MUTEX_TAG>::tls_data(_get_gdscript_cache_mutex()); +SafeBinaryMutex<GDScriptCache::BINARY_MUTEX_TAG> GDScriptCache::mutex; + void GDScriptCache::move_script(const String &p_from, const String &p_to) { if (singleton == nullptr || p_from == p_to) { return; @@ -369,7 +377,7 @@ Ref<GDScript> GDScriptCache::get_full_script(const String &p_path, Error &r_erro // Allowing lifting the lock might cause a script to be reloaded multiple times, // which, as a last resort deadlock prevention strategy, is a good tradeoff. - uint32_t allowance_id = WorkerThreadPool::thread_enter_unlock_allowance_zone(&singleton->mutex); + uint32_t allowance_id = WorkerThreadPool::thread_enter_unlock_allowance_zone(singleton->mutex); r_error = script->reload(true); WorkerThreadPool::thread_exit_unlock_allowance_zone(allowance_id); if (r_error) { diff --git a/modules/gdscript/gdscript_cache.h b/modules/gdscript/gdscript_cache.h index f7f2cd90e9..4903da92b4 100644 --- a/modules/gdscript/gdscript_cache.h +++ b/modules/gdscript/gdscript_cache.h @@ -34,7 +34,7 @@ #include "gdscript.h" #include "core/object/ref_counted.h" -#include "core/os/mutex.h" +#include "core/os/safe_binary_mutex.h" #include "core/templates/hash_map.h" #include "core/templates/hash_set.h" @@ -95,7 +95,12 @@ class GDScriptCache { bool cleared = false; - Mutex mutex; +public: + static const int BINARY_MUTEX_TAG = 2; + +private: + static SafeBinaryMutex<BINARY_MUTEX_TAG> mutex; + friend SafeBinaryMutex<BINARY_MUTEX_TAG> &_get_gdscript_cache_mutex(); public: static void move_script(const String &p_from, const String &p_to); diff --git a/modules/gdscript/gdscript_editor.cpp b/modules/gdscript/gdscript_editor.cpp index 822fc412b4..636339ef1d 100644 --- a/modules/gdscript/gdscript_editor.cpp +++ b/modules/gdscript/gdscript_editor.cpp @@ -97,8 +97,8 @@ Ref<Script> GDScriptLanguage::make_template(const String &p_template, const Stri } processed_template = processed_template.replace("_BASE_", p_base_class_name) - .replace("_CLASS_SNAKE_CASE_", p_class_name.to_snake_case().validate_identifier()) - .replace("_CLASS_", p_class_name.to_pascal_case().validate_identifier()) + .replace("_CLASS_SNAKE_CASE_", p_class_name.to_snake_case().validate_ascii_identifier()) + .replace("_CLASS_", p_class_name.to_pascal_case().validate_ascii_identifier()) .replace("_TS_", _get_indentation()); scr->set_source_code(processed_template); return scr; @@ -3303,11 +3303,36 @@ static void _find_call_arguments(GDScriptParser::CompletionContext &p_context, c case GDScriptParser::COMPLETION_SUBSCRIPT: { const GDScriptParser::SubscriptNode *subscript = static_cast<const GDScriptParser::SubscriptNode *>(completion_context.node); GDScriptCompletionIdentifier base; - if (!_guess_expression_type(completion_context, subscript->base, base)) { - break; - } + const bool res = _guess_expression_type(completion_context, subscript->base, base); + + // If the type is not known, we assume it is BUILTIN, since indices on arrays is the most common use case. + if (!subscript->is_attribute && (!res || base.type.kind == GDScriptParser::DataType::BUILTIN || base.type.is_variant())) { + if (base.value.get_type() == Variant::DICTIONARY) { + List<PropertyInfo> members; + base.value.get_property_list(&members); - _find_identifiers_in_base(base, false, false, options, 0); + for (const PropertyInfo &E : members) { + ScriptLanguage::CodeCompletionOption option(E.name.quote(quote_style), ScriptLanguage::CODE_COMPLETION_KIND_MEMBER, ScriptLanguage::LOCATION_LOCAL); + options.insert(option.display, option); + } + } + if (!subscript->index || subscript->index->type != GDScriptParser::Node::LITERAL) { + _find_identifiers(completion_context, false, options, 0); + } + } else if (res) { + if (!subscript->is_attribute) { + // Quote the options if they are not accessed as attribute. + + HashMap<String, ScriptLanguage::CodeCompletionOption> opt; + _find_identifiers_in_base(base, false, false, opt, 0); + for (const KeyValue<String, CodeCompletionOption> &E : opt) { + ScriptLanguage::CodeCompletionOption option(E.value.insert_text.quote(quote_style), E.value.kind, E.value.location); + options.insert(option.display, option); + } + } else { + _find_identifiers_in_base(base, false, false, options, 0); + } + } } break; case GDScriptParser::COMPLETION_TYPE_ATTRIBUTE: { if (!completion_context.current_class) { @@ -3461,7 +3486,7 @@ static void _find_call_arguments(GDScriptParser::CompletionContext &p_context, c // is not a valid identifier. bool path_needs_quote = false; for (const String &part : opt.split("/")) { - if (!part.is_valid_identifier()) { + if (!part.is_valid_ascii_identifier()) { path_needs_quote = true; break; } diff --git a/modules/gdscript/gdscript_function.h b/modules/gdscript/gdscript_function.h index 759e92d68c..ac4bab6d84 100644 --- a/modules/gdscript/gdscript_function.h +++ b/modules/gdscript/gdscript_function.h @@ -509,7 +509,7 @@ private: } profile; #endif - _FORCE_INLINE_ String _get_call_error(const Callable::CallError &p_err, const String &p_where, const Variant **argptrs) const; + _FORCE_INLINE_ String _get_call_error(const String &p_where, const Variant **p_argptrs, const Variant &p_ret, const Callable::CallError &p_err) const; Variant _get_default_variant_for_data_type(const GDScriptDataType &p_data_type); public: diff --git a/modules/gdscript/gdscript_parser.cpp b/modules/gdscript/gdscript_parser.cpp index ecef852b4b..582305d900 100644 --- a/modules/gdscript/gdscript_parser.cpp +++ b/modules/gdscript/gdscript_parser.cpp @@ -260,6 +260,7 @@ void GDScriptParser::override_completion_context(const Node *p_for_node, Complet context.current_line = tokenizer->get_cursor_line(); context.current_argument = p_argument; context.node = p_node; + context.parser = this; completion_context = context; } @@ -3122,6 +3123,12 @@ GDScriptParser::ExpressionNode *GDScriptParser::parse_subscript(ExpressionNode * subscript->base = p_previous_operand; subscript->index = parse_expression(false); +#ifdef TOOLS_ENABLED + if (subscript->index != nullptr && subscript->index->type == Node::LITERAL) { + override_completion_context(subscript->index, COMPLETION_SUBSCRIPT, subscript); + } +#endif + if (subscript->index == nullptr) { push_error(R"(Expected expression after "[".)"); } @@ -4086,7 +4093,7 @@ bool GDScriptParser::validate_annotation_arguments(AnnotationNode *p_annotation) return true; } -bool GDScriptParser::tool_annotation(const AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class) { +bool GDScriptParser::tool_annotation(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class) { #ifdef DEBUG_ENABLED if (_is_tool) { push_error(R"("@tool" annotation can only be used once.)", p_annotation); @@ -4097,7 +4104,7 @@ bool GDScriptParser::tool_annotation(const AnnotationNode *p_annotation, Node *p return true; } -bool GDScriptParser::icon_annotation(const AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class) { +bool GDScriptParser::icon_annotation(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class) { ERR_FAIL_COND_V_MSG(p_target->type != Node::CLASS, false, R"("@icon" annotation can only be applied to classes.)"); ERR_FAIL_COND_V(p_annotation->resolved_arguments.is_empty(), false); @@ -4128,7 +4135,7 @@ bool GDScriptParser::icon_annotation(const AnnotationNode *p_annotation, Node *p return true; } -bool GDScriptParser::onready_annotation(const AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class) { +bool GDScriptParser::onready_annotation(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class) { ERR_FAIL_COND_V_MSG(p_target->type != Node::VARIABLE, false, R"("@onready" annotation can only be applied to class variables.)"); if (current_class && !ClassDB::is_parent_class(current_class->get_datatype().native_type, SNAME("Node"))) { @@ -4261,7 +4268,7 @@ static StringName _find_narrowest_native_or_global_class(const GDScriptParser::D } template <PropertyHint t_hint, Variant::Type t_type> -bool GDScriptParser::export_annotations(const AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class) { +bool GDScriptParser::export_annotations(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class) { ERR_FAIL_COND_V_MSG(p_target->type != Node::VARIABLE, false, vformat(R"("%s" annotation can only be applied to variables.)", p_annotation->name)); ERR_FAIL_NULL_V(p_class, false); @@ -4500,7 +4507,7 @@ bool GDScriptParser::export_annotations(const AnnotationNode *p_annotation, Node // For `@export_storage` and `@export_custom`, there is no need to check the variable type, argument values, // or handle array exports in a special way, so they are implemented as separate methods. -bool GDScriptParser::export_storage_annotation(const AnnotationNode *p_annotation, Node *p_node, ClassNode *p_class) { +bool GDScriptParser::export_storage_annotation(AnnotationNode *p_annotation, Node *p_node, ClassNode *p_class) { ERR_FAIL_COND_V_MSG(p_node->type != Node::VARIABLE, false, vformat(R"("%s" annotation can only be applied to variables.)", p_annotation->name)); VariableNode *variable = static_cast<VariableNode *>(p_node); @@ -4522,7 +4529,7 @@ bool GDScriptParser::export_storage_annotation(const AnnotationNode *p_annotatio return true; } -bool GDScriptParser::export_custom_annotation(const AnnotationNode *p_annotation, Node *p_node, ClassNode *p_class) { +bool GDScriptParser::export_custom_annotation(AnnotationNode *p_annotation, Node *p_node, ClassNode *p_class) { ERR_FAIL_COND_V_MSG(p_node->type != Node::VARIABLE, false, vformat(R"("%s" annotation can only be applied to variables.)", p_annotation->name)); ERR_FAIL_COND_V_MSG(p_annotation->resolved_arguments.size() < 2, false, R"(Annotation "@export_custom" requires 2 arguments.)"); @@ -4551,31 +4558,29 @@ bool GDScriptParser::export_custom_annotation(const AnnotationNode *p_annotation } template <PropertyUsageFlags t_usage> -bool GDScriptParser::export_group_annotations(const AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class) { - AnnotationNode *annotation = const_cast<AnnotationNode *>(p_annotation); - - if (annotation->resolved_arguments.is_empty()) { +bool GDScriptParser::export_group_annotations(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class) { + if (p_annotation->resolved_arguments.is_empty()) { return false; } - annotation->export_info.name = annotation->resolved_arguments[0]; + p_annotation->export_info.name = p_annotation->resolved_arguments[0]; switch (t_usage) { case PROPERTY_USAGE_CATEGORY: { - annotation->export_info.usage = t_usage; + p_annotation->export_info.usage = t_usage; } break; case PROPERTY_USAGE_GROUP: { - annotation->export_info.usage = t_usage; - if (annotation->resolved_arguments.size() == 2) { - annotation->export_info.hint_string = annotation->resolved_arguments[1]; + p_annotation->export_info.usage = t_usage; + if (p_annotation->resolved_arguments.size() == 2) { + p_annotation->export_info.hint_string = p_annotation->resolved_arguments[1]; } } break; case PROPERTY_USAGE_SUBGROUP: { - annotation->export_info.usage = t_usage; - if (annotation->resolved_arguments.size() == 2) { - annotation->export_info.hint_string = annotation->resolved_arguments[1]; + p_annotation->export_info.usage = t_usage; + if (p_annotation->resolved_arguments.size() == 2) { + p_annotation->export_info.hint_string = p_annotation->resolved_arguments[1]; } } break; } @@ -4583,7 +4588,7 @@ bool GDScriptParser::export_group_annotations(const AnnotationNode *p_annotation return true; } -bool GDScriptParser::warning_annotations(const AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class) { +bool GDScriptParser::warning_annotations(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class) { #ifndef DEBUG_ENABLED // Only available in debug builds. return true; @@ -4658,7 +4663,7 @@ bool GDScriptParser::warning_annotations(const AnnotationNode *p_annotation, Nod #endif // DEBUG_ENABLED } -bool GDScriptParser::rpc_annotation(const AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class) { +bool GDScriptParser::rpc_annotation(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class) { ERR_FAIL_COND_V_MSG(p_target->type != Node::FUNCTION, false, vformat(R"("%s" annotation can only be applied to functions.)", p_annotation->name)); FunctionNode *function = static_cast<FunctionNode *>(p_target); @@ -4719,7 +4724,7 @@ bool GDScriptParser::rpc_annotation(const AnnotationNode *p_annotation, Node *p_ return true; } -bool GDScriptParser::static_unload_annotation(const AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class) { +bool GDScriptParser::static_unload_annotation(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class) { ERR_FAIL_COND_V_MSG(p_target->type != Node::CLASS, false, vformat(R"("%s" annotation can only be applied to classes.)", p_annotation->name)); ClassNode *class_node = static_cast<ClassNode *>(p_target); if (class_node->annotated_static_unload) { diff --git a/modules/gdscript/gdscript_parser.h b/modules/gdscript/gdscript_parser.h index 2999fb11e4..43c5a48fa7 100644 --- a/modules/gdscript/gdscript_parser.h +++ b/modules/gdscript/gdscript_parser.h @@ -1371,7 +1371,7 @@ private: bool in_lambda = false; bool lambda_ended = false; // Marker for when a lambda ends, to apply an end of statement if needed. - typedef bool (GDScriptParser::*AnnotationAction)(const AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class); + typedef bool (GDScriptParser::*AnnotationAction)(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class); struct AnnotationInfo { enum TargetKind { NONE = 0, @@ -1495,18 +1495,18 @@ private: static bool register_annotation(const MethodInfo &p_info, uint32_t p_target_kinds, AnnotationAction p_apply, const Vector<Variant> &p_default_arguments = Vector<Variant>(), bool p_is_vararg = false); bool validate_annotation_arguments(AnnotationNode *p_annotation); void clear_unused_annotations(); - bool tool_annotation(const AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class); - bool icon_annotation(const AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class); - bool onready_annotation(const AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class); + bool tool_annotation(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class); + bool icon_annotation(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class); + bool onready_annotation(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class); template <PropertyHint t_hint, Variant::Type t_type> - bool export_annotations(const AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class); - bool export_storage_annotation(const AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class); - bool export_custom_annotation(const AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class); + bool export_annotations(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class); + bool export_storage_annotation(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class); + bool export_custom_annotation(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class); template <PropertyUsageFlags t_usage> - bool export_group_annotations(const AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class); - bool warning_annotations(const AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class); - bool rpc_annotation(const AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class); - bool static_unload_annotation(const AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class); + bool export_group_annotations(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class); + bool warning_annotations(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class); + bool rpc_annotation(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class); + bool static_unload_annotation(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class); // Statements. Node *parse_statement(); VariableNode *parse_variable(bool p_is_static); diff --git a/modules/gdscript/gdscript_vm.cpp b/modules/gdscript/gdscript_vm.cpp index 5eab8a6306..ddb0cf9502 100644 --- a/modules/gdscript/gdscript_vm.cpp +++ b/modules/gdscript/gdscript_vm.cpp @@ -134,38 +134,36 @@ Variant GDScriptFunction::_get_default_variant_for_data_type(const GDScriptDataT return Variant(); } -String GDScriptFunction::_get_call_error(const Callable::CallError &p_err, const String &p_where, const Variant **argptrs) const { - String err_text; - - if (p_err.error == Callable::CallError::CALL_ERROR_INVALID_ARGUMENT) { - int errorarg = p_err.argument; - ERR_FAIL_COND_V_MSG(errorarg < 0 || argptrs[errorarg] == nullptr, "GDScript bug (please report): Invalid CallError argument index or null pointer.", "Invalid CallError argument index or null pointer."); - // Handle the Object to Object case separately as we don't have further class details. +String GDScriptFunction::_get_call_error(const String &p_where, const Variant **p_argptrs, const Variant &p_ret, const Callable::CallError &p_err) const { + switch (p_err.error) { + case Callable::CallError::CALL_OK: + return String(); + case Callable::CallError::CALL_ERROR_INVALID_METHOD: + if (p_ret.get_type() == Variant::STRING && !p_ret.operator String().is_empty()) { + return "Invalid call " + p_where + ": " + p_ret.operator String(); + } + return "Invalid call. Nonexistent " + p_where + "."; + case Callable::CallError::CALL_ERROR_INVALID_ARGUMENT: + ERR_FAIL_COND_V_MSG(p_err.argument < 0 || p_argptrs[p_err.argument] == nullptr, "Bug: Invalid CallError argument index or null pointer.", "Bug: Invalid CallError argument index or null pointer."); + // Handle the Object to Object case separately as we don't have further class details. #ifdef DEBUG_ENABLED - if (p_err.expected == Variant::OBJECT && argptrs[errorarg]->get_type() == p_err.expected) { - err_text = "Invalid type in " + p_where + ". The Object-derived class of argument " + itos(errorarg + 1) + " (" + _get_var_type(argptrs[errorarg]) + ") is not a subclass of the expected argument class."; - } else if (p_err.expected == Variant::ARRAY && argptrs[errorarg]->get_type() == p_err.expected) { - err_text = "Invalid type in " + p_where + ". The array of argument " + itos(errorarg + 1) + " (" + _get_var_type(argptrs[errorarg]) + ") does not have the same element type as the expected typed array argument."; - } else + if (p_err.expected == Variant::OBJECT && p_argptrs[p_err.argument]->get_type() == p_err.expected) { + return "Invalid type in " + p_where + ". The Object-derived class of argument " + itos(p_err.argument + 1) + " (" + _get_var_type(p_argptrs[p_err.argument]) + ") is not a subclass of the expected argument class."; + } + if (p_err.expected == Variant::ARRAY && p_argptrs[p_err.argument]->get_type() == p_err.expected) { + return "Invalid type in " + p_where + ". The array of argument " + itos(p_err.argument + 1) + " (" + _get_var_type(p_argptrs[p_err.argument]) + ") does not have the same element type as the expected typed array argument."; + } #endif // DEBUG_ENABLED - { - err_text = "Invalid type in " + p_where + ". Cannot convert argument " + itos(errorarg + 1) + " from " + Variant::get_type_name(argptrs[errorarg]->get_type()) + " to " + Variant::get_type_name(Variant::Type(p_err.expected)) + "."; - } - } else if (p_err.error == Callable::CallError::CALL_ERROR_TOO_MANY_ARGUMENTS) { - err_text = "Invalid call to " + p_where + ". Expected " + itos(p_err.expected) + " arguments."; - } else if (p_err.error == Callable::CallError::CALL_ERROR_TOO_FEW_ARGUMENTS) { - err_text = "Invalid call to " + p_where + ". Expected " + itos(p_err.expected) + " arguments."; - } else if (p_err.error == Callable::CallError::CALL_ERROR_INVALID_METHOD) { - err_text = "Invalid call. Nonexistent " + p_where + "."; - } else if (p_err.error == Callable::CallError::CALL_ERROR_INSTANCE_IS_NULL) { - err_text = "Attempt to call " + p_where + " on a null instance."; - } else if (p_err.error == Callable::CallError::CALL_ERROR_METHOD_NOT_CONST) { - err_text = "Attempt to call " + p_where + " on a const instance."; - } else { - err_text = "Bug, call error: #" + itos(p_err.error); + return "Invalid type in " + p_where + ". Cannot convert argument " + itos(p_err.argument + 1) + " from " + Variant::get_type_name(p_argptrs[p_err.argument]->get_type()) + " to " + Variant::get_type_name(Variant::Type(p_err.expected)) + "."; + case Callable::CallError::CALL_ERROR_TOO_MANY_ARGUMENTS: + case Callable::CallError::CALL_ERROR_TOO_FEW_ARGUMENTS: + return "Invalid call to " + p_where + ". Expected " + itos(p_err.expected) + " arguments."; + case Callable::CallError::CALL_ERROR_INSTANCE_IS_NULL: + return "Attempt to call " + p_where + " on a null instance."; + case Callable::CallError::CALL_ERROR_METHOD_NOT_CONST: + return "Attempt to call " + p_where + " on a const instance."; } - - return err_text; + return "Bug: Invalid call error code " + itos(p_err.error) + "."; } void (*type_init_function_table[])(Variant *) = { @@ -1608,7 +1606,7 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a #ifdef DEBUG_ENABLED if (err.error != Callable::CallError::CALL_OK) { - err_text = _get_call_error(err, "'" + Variant::get_type_name(t) + "' constructor", (const Variant **)argptrs); + err_text = _get_call_error("'" + Variant::get_type_name(t) + "' constructor", (const Variant **)argptrs, *dst, err); OPCODE_BREAK; } #endif @@ -1744,10 +1742,12 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a StringName base_class = base_obj ? base_obj->get_class_name() : StringName(); #endif + Variant temp_ret; Callable::CallError err; if (call_ret) { GET_INSTRUCTION_ARG(ret, argc + 1); - base->callp(*methodname, (const Variant **)argptrs, argc, *ret, err); + base->callp(*methodname, (const Variant **)argptrs, argc, temp_ret, err); + *ret = temp_ret; #ifdef DEBUG_ENABLED if (ret->get_type() == Variant::NIL) { if (base_type == Variant::OBJECT) { @@ -1776,8 +1776,7 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a } #endif } else { - Variant ret; - base->callp(*methodname, (const Variant **)argptrs, argc, ret, err); + base->callp(*methodname, (const Variant **)argptrs, argc, temp_ret, err); } #ifdef DEBUG_ENABLED @@ -1822,7 +1821,7 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a } } } - err_text = _get_call_error(err, "function '" + methodstr + (is_callable ? "" : "' in base '" + basestr) + "'", (const Variant **)argptrs); + err_text = _get_call_error("function '" + methodstr + (is_callable ? "" : "' in base '" + basestr) + "'", (const Variant **)argptrs, temp_ret, err); OPCODE_BREAK; } #endif @@ -1868,12 +1867,14 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a } #endif + Variant temp_ret; Callable::CallError err; if (call_ret) { GET_INSTRUCTION_ARG(ret, argc + 1); - *ret = method->call(base_obj, (const Variant **)argptrs, argc, err); + temp_ret = method->call(base_obj, (const Variant **)argptrs, argc, err); + *ret = temp_ret; } else { - method->call(base_obj, (const Variant **)argptrs, argc, err); + temp_ret = method->call(base_obj, (const Variant **)argptrs, argc, err); } #ifdef DEBUG_ENABLED @@ -1906,7 +1907,7 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a } } } - err_text = _get_call_error(err, "function '" + methodstr + "' in base '" + basestr + "'", (const Variant **)argptrs); + err_text = _get_call_error("function '" + methodstr + "' in base '" + basestr + "'", (const Variant **)argptrs, temp_ret, err); OPCODE_BREAK; } #endif @@ -1939,7 +1940,7 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a #ifdef DEBUG_ENABLED if (err.error != Callable::CallError::CALL_OK) { - err_text = _get_call_error(err, "static function '" + methodname->operator String() + "' in type '" + Variant::get_type_name(builtin_type) + "'", argptrs); + err_text = _get_call_error("static function '" + methodname->operator String() + "' in type '" + Variant::get_type_name(builtin_type) + "'", argptrs, *ret, err); OPCODE_BREAK; } #endif @@ -1983,7 +1984,7 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a #endif if (err.error != Callable::CallError::CALL_OK) { - err_text = _get_call_error(err, "static function '" + method->get_name().operator String() + "' in type '" + method->get_instance_class().operator String() + "'", argptrs); + err_text = _get_call_error("static function '" + method->get_name().operator String() + "' in type '" + method->get_instance_class().operator String() + "'", argptrs, *ret, err); OPCODE_BREAK; } @@ -2214,7 +2215,7 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a // Call provided error string. err_text = vformat(R"*(Error calling utility function "%s()": %s)*", methodstr, *dst); } else { - err_text = _get_call_error(err, vformat(R"*(utility function "%s()")*", methodstr), (const Variant **)argptrs); + err_text = _get_call_error(vformat(R"*(utility function "%s()")*", methodstr), (const Variant **)argptrs, *dst, err); } OPCODE_BREAK; } @@ -2271,7 +2272,7 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a // Call provided error string. err_text = vformat(R"*(Error calling GDScript utility function "%s()": %s)*", methodstr, *dst); } else { - err_text = _get_call_error(err, vformat(R"*(GDScript utility function "%s()")*", methodstr), (const Variant **)argptrs); + err_text = _get_call_error(vformat(R"*(GDScript utility function "%s()")*", methodstr), (const Variant **)argptrs, *dst, err); } OPCODE_BREAK; } @@ -2338,7 +2339,7 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a if (err.error != Callable::CallError::CALL_OK) { String methodstr = *methodname; - err_text = _get_call_error(err, "function '" + methodstr + "'", (const Variant **)argptrs); + err_text = _get_call_error("function '" + methodstr + "'", (const Variant **)argptrs, *dst, err); OPCODE_BREAK; } diff --git a/modules/gdscript/gdscript_warning.cpp b/modules/gdscript/gdscript_warning.cpp index e8fb1d94b3..4ffb4bd9d1 100644 --- a/modules/gdscript/gdscript_warning.cpp +++ b/modules/gdscript/gdscript_warning.cpp @@ -109,6 +109,8 @@ String GDScriptWarning::get_message() const { case STATIC_CALLED_ON_INSTANCE: CHECK_SYMBOLS(2); return vformat(R"*(The function "%s()" is a static function but was called from an instance. Instead, it should be directly called from the type: "%s.%s()".)*", symbols[0], symbols[1], symbols[0]); + case MISSING_TOOL: + return R"(The base class script has the "@tool" annotation, but this script does not have it.)"; case REDUNDANT_STATIC_UNLOAD: return R"(The "@static_unload" annotation is redundant because the file does not have a class with static variables.)"; case REDUNDANT_AWAIT: @@ -219,6 +221,7 @@ String GDScriptWarning::get_name_from_code(Code p_code) { "UNSAFE_VOID_RETURN", "RETURN_VALUE_DISCARDED", "STATIC_CALLED_ON_INSTANCE", + "MISSING_TOOL", "REDUNDANT_STATIC_UNLOAD", "REDUNDANT_AWAIT", "ASSERT_ALWAYS_TRUE", diff --git a/modules/gdscript/gdscript_warning.h b/modules/gdscript/gdscript_warning.h index 1c806bb4e2..ffcf00a830 100644 --- a/modules/gdscript/gdscript_warning.h +++ b/modules/gdscript/gdscript_warning.h @@ -70,6 +70,7 @@ public: UNSAFE_VOID_RETURN, // Function returns void but returned a call to a function that can't be type checked. RETURN_VALUE_DISCARDED, // Function call returns something but the value isn't used. STATIC_CALLED_ON_INSTANCE, // A static method was called on an instance of a class instead of on the class itself. + MISSING_TOOL, // The base class script has the "@tool" annotation, but this script does not have it. REDUNDANT_STATIC_UNLOAD, // The `@static_unload` annotation is used but the class does not have static data. REDUNDANT_AWAIT, // await is used but expression is synchronous (not a signal nor a coroutine). ASSERT_ALWAYS_TRUE, // Expression for assert argument is always true. @@ -123,6 +124,7 @@ public: WARN, // UNSAFE_VOID_RETURN IGNORE, // RETURN_VALUE_DISCARDED // Too spammy by default on common cases (connect, Tween, etc.). WARN, // STATIC_CALLED_ON_INSTANCE + WARN, // MISSING_TOOL WARN, // REDUNDANT_STATIC_UNLOAD WARN, // REDUNDANT_AWAIT WARN, // ASSERT_ALWAYS_TRUE diff --git a/modules/gdscript/tests/scripts/analyzer/features/assymetric_assignment_good.gd b/modules/gdscript/tests/scripts/analyzer/features/assymetric_assignment_good.gd index efd8ad6edb..60bcde4b8c 100644 --- a/modules/gdscript/tests/scripts/analyzer/features/assymetric_assignment_good.gd +++ b/modules/gdscript/tests/scripts/analyzer/features/assymetric_assignment_good.gd @@ -3,14 +3,13 @@ const const_color: Color = 'red' func func_color(arg_color: Color = 'blue') -> bool: return arg_color == Color.BLUE -@warning_ignore("assert_always_true") func test(): - assert(const_color == Color.RED) + Utils.check(const_color == Color.RED) - assert(func_color() == true) - assert(func_color('blue') == true) + Utils.check(func_color() == true) + Utils.check(func_color('blue') == true) var var_color: Color = 'green' - assert(var_color == Color.GREEN) + Utils.check(var_color == Color.GREEN) print('ok') diff --git a/modules/gdscript/tests/scripts/analyzer/features/const_conversions.gd b/modules/gdscript/tests/scripts/analyzer/features/const_conversions.gd index bed9dd0e96..5318d11f33 100644 --- a/modules/gdscript/tests/scripts/analyzer/features/const_conversions.gd +++ b/modules/gdscript/tests/scripts/analyzer/features/const_conversions.gd @@ -5,20 +5,19 @@ const const_float_cast: float = 76 as float const const_packed_empty: PackedFloat64Array = [] const const_packed_ints: PackedFloat64Array = [52] -@warning_ignore("assert_always_true") func test(): - assert(typeof(const_float_int) == TYPE_FLOAT) - assert(str(const_float_int) == '19') - assert(typeof(const_float_plus) == TYPE_FLOAT) - assert(str(const_float_plus) == '34') - assert(typeof(const_float_cast) == TYPE_FLOAT) - assert(str(const_float_cast) == '76') + Utils.check(typeof(const_float_int) == TYPE_FLOAT) + Utils.check(str(const_float_int) == '19') + Utils.check(typeof(const_float_plus) == TYPE_FLOAT) + Utils.check(str(const_float_plus) == '34') + Utils.check(typeof(const_float_cast) == TYPE_FLOAT) + Utils.check(str(const_float_cast) == '76') - assert(typeof(const_packed_empty) == TYPE_PACKED_FLOAT64_ARRAY) - assert(str(const_packed_empty) == '[]') - assert(typeof(const_packed_ints) == TYPE_PACKED_FLOAT64_ARRAY) - assert(str(const_packed_ints) == '[52]') - assert(typeof(const_packed_ints[0]) == TYPE_FLOAT) - assert(str(const_packed_ints[0]) == '52') + Utils.check(typeof(const_packed_empty) == TYPE_PACKED_FLOAT64_ARRAY) + Utils.check(str(const_packed_empty) == '[]') + Utils.check(typeof(const_packed_ints) == TYPE_PACKED_FLOAT64_ARRAY) + Utils.check(str(const_packed_ints) == '[52]') + Utils.check(typeof(const_packed_ints[0]) == TYPE_FLOAT) + Utils.check(str(const_packed_ints[0]) == '52') print('ok') diff --git a/modules/gdscript/tests/scripts/analyzer/features/enums_in_range_call.gd b/modules/gdscript/tests/scripts/analyzer/features/enums_in_range_call.gd index d2d9d04508..a569488d3c 100644 --- a/modules/gdscript/tests/scripts/analyzer/features/enums_in_range_call.gd +++ b/modules/gdscript/tests/scripts/analyzer/features/enums_in_range_call.gd @@ -5,5 +5,5 @@ func test(): for value in range(E.E0, E.E3): var inferable := value total += inferable - assert(total == 0 + 1 + 2) + Utils.check(total == 0 + 1 + 2) print('ok') diff --git a/modules/gdscript/tests/scripts/analyzer/features/export_enum_as_dictionary.gd b/modules/gdscript/tests/scripts/analyzer/features/export_enum_as_dictionary.gd index 39f490c4b3..ec89226328 100644 --- a/modules/gdscript/tests/scripts/analyzer/features/export_enum_as_dictionary.gd +++ b/modules/gdscript/tests/scripts/analyzer/features/export_enum_as_dictionary.gd @@ -2,8 +2,6 @@ class_name TestExportEnumAsDictionary enum MyEnum {A, B, C} -const Utils = preload("../../utils.notest.gd") - @export var test_1 = MyEnum @export var test_2 = MyEnum.A @export var test_3 := MyEnum diff --git a/modules/gdscript/tests/scripts/analyzer/features/for_range_usage.gd b/modules/gdscript/tests/scripts/analyzer/features/for_range_usage.gd index 4a7f10f1ee..9ce0782d5c 100644 --- a/modules/gdscript/tests/scripts/analyzer/features/for_range_usage.gd +++ b/modules/gdscript/tests/scripts/analyzer/features/for_range_usage.gd @@ -3,5 +3,5 @@ func test(): var result := '' for i in range(array.size(), 0, -1): result += str(array[i - 1]) - assert(result == '963') + Utils.check(result == '963') print('ok') diff --git a/modules/gdscript/tests/scripts/analyzer/features/function_match_parent_signature_with_extra_parameters.gd b/modules/gdscript/tests/scripts/analyzer/features/function_match_parent_signature_with_extra_parameters.gd index d678f3acfc..e0cbdacb38 100644 --- a/modules/gdscript/tests/scripts/analyzer/features/function_match_parent_signature_with_extra_parameters.gd +++ b/modules/gdscript/tests/scripts/analyzer/features/function_match_parent_signature_with_extra_parameters.gd @@ -2,11 +2,11 @@ func test(): var instance := Parent.new() var result := instance.my_function(1) print(result) - assert(result == 1) + Utils.check(result == 1) instance = Child.new() result = instance.my_function(2) print(result) - assert(result == 0) + Utils.check(result == 0) class Parent: func my_function(par1: int) -> int: diff --git a/modules/gdscript/tests/scripts/analyzer/features/return_conversions.gd b/modules/gdscript/tests/scripts/analyzer/features/return_conversions.gd index 0b1576e66e..cbe8e9da34 100644 --- a/modules/gdscript/tests/scripts/analyzer/features/return_conversions.gd +++ b/modules/gdscript/tests/scripts/analyzer/features/return_conversions.gd @@ -8,27 +8,27 @@ func convert_var_array_to_packed() -> PackedStringArray: var array := ['79']; re func test(): var converted_literal_int := convert_literal_int_to_float() - assert(typeof(converted_literal_int) == TYPE_FLOAT) - assert(converted_literal_int == 76.0) + Utils.check(typeof(converted_literal_int) == TYPE_FLOAT) + Utils.check(converted_literal_int == 76.0) var converted_arg_int := convert_arg_int_to_float(36) - assert(typeof(converted_arg_int) == TYPE_FLOAT) - assert(converted_arg_int == 36.0) + Utils.check(typeof(converted_arg_int) == TYPE_FLOAT) + Utils.check(converted_arg_int == 36.0) var converted_var_int := convert_var_int_to_float() - assert(typeof(converted_var_int) == TYPE_FLOAT) - assert(converted_var_int == 59.0) + Utils.check(typeof(converted_var_int) == TYPE_FLOAT) + Utils.check(converted_var_int == 59.0) var converted_literal_array := convert_literal_array_to_packed() - assert(typeof(converted_literal_array) == TYPE_PACKED_STRING_ARRAY) - assert(str(converted_literal_array) == '["46"]') + Utils.check(typeof(converted_literal_array) == TYPE_PACKED_STRING_ARRAY) + Utils.check(str(converted_literal_array) == '["46"]') var converted_arg_array := convert_arg_array_to_packed(['91']) - assert(typeof(converted_arg_array) == TYPE_PACKED_STRING_ARRAY) - assert(str(converted_arg_array) == '["91"]') + Utils.check(typeof(converted_arg_array) == TYPE_PACKED_STRING_ARRAY) + Utils.check(str(converted_arg_array) == '["91"]') var converted_var_array := convert_var_array_to_packed() - assert(typeof(converted_var_array) == TYPE_PACKED_STRING_ARRAY) - assert(str(converted_var_array) == '["79"]') + Utils.check(typeof(converted_var_array) == TYPE_PACKED_STRING_ARRAY) + Utils.check(str(converted_var_array) == '["79"]') print('ok') diff --git a/modules/gdscript/tests/scripts/analyzer/features/ternary_hard_infer.gd b/modules/gdscript/tests/scripts/analyzer/features/ternary_hard_infer.gd index 44ca5f4dd0..d49acaacd3 100644 --- a/modules/gdscript/tests/scripts/analyzer/features/ternary_hard_infer.gd +++ b/modules/gdscript/tests/scripts/analyzer/features/ternary_hard_infer.gd @@ -2,7 +2,7 @@ func test(): var left_hard_int := 1 var right_hard_int := 2 var result_hard_int := left_hard_int if true else right_hard_int - assert(result_hard_int == 1) + Utils.check(result_hard_int == 1) @warning_ignore("inference_on_variant") var left_hard_variant := 1 as Variant @@ -10,6 +10,6 @@ func test(): var right_hard_variant := 2.0 as Variant @warning_ignore("inference_on_variant") var result_hard_variant := left_hard_variant if true else right_hard_variant - assert(result_hard_variant == 1) + Utils.check(result_hard_variant == 1) print('ok') diff --git a/modules/gdscript/tests/scripts/analyzer/features/type_test_usage.gd b/modules/gdscript/tests/scripts/analyzer/features/type_test_usage.gd index 12dc0b93df..ee30f01dfb 100644 --- a/modules/gdscript/tests/scripts/analyzer/features/type_test_usage.gd +++ b/modules/gdscript/tests/scripts/analyzer/features/type_test_usage.gd @@ -4,124 +4,123 @@ class A extends RefCounted: class B extends A: pass -@warning_ignore("assert_always_true") func test(): var builtin: Variant = 3 - assert((builtin is Variant) == true) - assert((builtin is int) == true) - assert(is_instance_of(builtin, TYPE_INT) == true) - assert((builtin is float) == false) - assert(is_instance_of(builtin, TYPE_FLOAT) == false) + Utils.check((builtin is Variant) == true) + Utils.check((builtin is int) == true) + Utils.check(is_instance_of(builtin, TYPE_INT) == true) + Utils.check((builtin is float) == false) + Utils.check(is_instance_of(builtin, TYPE_FLOAT) == false) const const_builtin: Variant = 3 - assert((const_builtin is Variant) == true) - assert((const_builtin is int) == true) - assert(is_instance_of(const_builtin, TYPE_INT) == true) - assert((const_builtin is float) == false) - assert(is_instance_of(const_builtin, TYPE_FLOAT) == false) + Utils.check((const_builtin is Variant) == true) + Utils.check((const_builtin is int) == true) + Utils.check(is_instance_of(const_builtin, TYPE_INT) == true) + Utils.check((const_builtin is float) == false) + Utils.check(is_instance_of(const_builtin, TYPE_FLOAT) == false) var int_array: Variant = [] as Array[int] - assert((int_array is Variant) == true) - assert((int_array is Array) == true) - assert(is_instance_of(int_array, TYPE_ARRAY) == true) - assert((int_array is Array[int]) == true) - assert((int_array is Array[float]) == false) - assert((int_array is int) == false) - assert(is_instance_of(int_array, TYPE_INT) == false) + Utils.check((int_array is Variant) == true) + Utils.check((int_array is Array) == true) + Utils.check(is_instance_of(int_array, TYPE_ARRAY) == true) + Utils.check((int_array is Array[int]) == true) + Utils.check((int_array is Array[float]) == false) + Utils.check((int_array is int) == false) + Utils.check(is_instance_of(int_array, TYPE_INT) == false) var const_int_array: Variant = [] as Array[int] - assert((const_int_array is Variant) == true) - assert((const_int_array is Array) == true) - assert(is_instance_of(const_int_array, TYPE_ARRAY) == true) - assert((const_int_array is Array[int]) == true) - assert((const_int_array is Array[float]) == false) - assert((const_int_array is int) == false) - assert(is_instance_of(const_int_array, TYPE_INT) == false) + Utils.check((const_int_array is Variant) == true) + Utils.check((const_int_array is Array) == true) + Utils.check(is_instance_of(const_int_array, TYPE_ARRAY) == true) + Utils.check((const_int_array is Array[int]) == true) + Utils.check((const_int_array is Array[float]) == false) + Utils.check((const_int_array is int) == false) + Utils.check(is_instance_of(const_int_array, TYPE_INT) == false) var b_array: Variant = [] as Array[B] - assert((b_array is Variant) == true) - assert((b_array is Array) == true) - assert(is_instance_of(b_array, TYPE_ARRAY) == true) - assert((b_array is Array[B]) == true) - assert((b_array is Array[A]) == false) - assert((b_array is Array[int]) == false) - assert((b_array is int) == false) - assert(is_instance_of(b_array, TYPE_INT) == false) + Utils.check((b_array is Variant) == true) + Utils.check((b_array is Array) == true) + Utils.check(is_instance_of(b_array, TYPE_ARRAY) == true) + Utils.check((b_array is Array[B]) == true) + Utils.check((b_array is Array[A]) == false) + Utils.check((b_array is Array[int]) == false) + Utils.check((b_array is int) == false) + Utils.check(is_instance_of(b_array, TYPE_INT) == false) var const_b_array: Variant = [] as Array[B] - assert((const_b_array is Variant) == true) - assert((const_b_array is Array) == true) - assert(is_instance_of(const_b_array, TYPE_ARRAY) == true) - assert((const_b_array is Array[B]) == true) - assert((const_b_array is Array[A]) == false) - assert((const_b_array is Array[int]) == false) - assert((const_b_array is int) == false) - assert(is_instance_of(const_b_array, TYPE_INT) == false) + Utils.check((const_b_array is Variant) == true) + Utils.check((const_b_array is Array) == true) + Utils.check(is_instance_of(const_b_array, TYPE_ARRAY) == true) + Utils.check((const_b_array is Array[B]) == true) + Utils.check((const_b_array is Array[A]) == false) + Utils.check((const_b_array is Array[int]) == false) + Utils.check((const_b_array is int) == false) + Utils.check(is_instance_of(const_b_array, TYPE_INT) == false) var native: Variant = RefCounted.new() - assert((native is Variant) == true) - assert((native is Object) == true) - assert(is_instance_of(native, TYPE_OBJECT) == true) - assert(is_instance_of(native, Object) == true) - assert((native is RefCounted) == true) - assert(is_instance_of(native, RefCounted) == true) - assert((native is Node) == false) - assert(is_instance_of(native, Node) == false) - assert((native is int) == false) - assert(is_instance_of(native, TYPE_INT) == false) + Utils.check((native is Variant) == true) + Utils.check((native is Object) == true) + Utils.check(is_instance_of(native, TYPE_OBJECT) == true) + Utils.check(is_instance_of(native, Object) == true) + Utils.check((native is RefCounted) == true) + Utils.check(is_instance_of(native, RefCounted) == true) + Utils.check((native is Node) == false) + Utils.check(is_instance_of(native, Node) == false) + Utils.check((native is int) == false) + Utils.check(is_instance_of(native, TYPE_INT) == false) var a_script: Variant = A.new() - assert((a_script is Variant) == true) - assert((a_script is Object) == true) - assert(is_instance_of(a_script, TYPE_OBJECT) == true) - assert(is_instance_of(a_script, Object) == true) - assert((a_script is RefCounted) == true) - assert(is_instance_of(a_script, RefCounted) == true) - assert((a_script is A) == true) - assert(is_instance_of(a_script, A) == true) - assert((a_script is B) == false) - assert(is_instance_of(a_script, B) == false) - assert((a_script is Node) == false) - assert(is_instance_of(a_script, Node) == false) - assert((a_script is int) == false) - assert(is_instance_of(a_script, TYPE_INT) == false) + Utils.check((a_script is Variant) == true) + Utils.check((a_script is Object) == true) + Utils.check(is_instance_of(a_script, TYPE_OBJECT) == true) + Utils.check(is_instance_of(a_script, Object) == true) + Utils.check((a_script is RefCounted) == true) + Utils.check(is_instance_of(a_script, RefCounted) == true) + Utils.check((a_script is A) == true) + Utils.check(is_instance_of(a_script, A) == true) + Utils.check((a_script is B) == false) + Utils.check(is_instance_of(a_script, B) == false) + Utils.check((a_script is Node) == false) + Utils.check(is_instance_of(a_script, Node) == false) + Utils.check((a_script is int) == false) + Utils.check(is_instance_of(a_script, TYPE_INT) == false) var b_script: Variant = B.new() - assert((b_script is Variant) == true) - assert((b_script is Object) == true) - assert(is_instance_of(b_script, TYPE_OBJECT) == true) - assert(is_instance_of(b_script, Object) == true) - assert((b_script is RefCounted) == true) - assert(is_instance_of(b_script, RefCounted) == true) - assert((b_script is A) == true) - assert(is_instance_of(b_script, A) == true) - assert((b_script is B) == true) - assert(is_instance_of(b_script, B) == true) - assert((b_script is Node) == false) - assert(is_instance_of(b_script, Node) == false) - assert((b_script is int) == false) - assert(is_instance_of(b_script, TYPE_INT) == false) + Utils.check((b_script is Variant) == true) + Utils.check((b_script is Object) == true) + Utils.check(is_instance_of(b_script, TYPE_OBJECT) == true) + Utils.check(is_instance_of(b_script, Object) == true) + Utils.check((b_script is RefCounted) == true) + Utils.check(is_instance_of(b_script, RefCounted) == true) + Utils.check((b_script is A) == true) + Utils.check(is_instance_of(b_script, A) == true) + Utils.check((b_script is B) == true) + Utils.check(is_instance_of(b_script, B) == true) + Utils.check((b_script is Node) == false) + Utils.check(is_instance_of(b_script, Node) == false) + Utils.check((b_script is int) == false) + Utils.check(is_instance_of(b_script, TYPE_INT) == false) var var_null: Variant = null - assert((var_null is Variant) == true) - assert((var_null is int) == false) - assert(is_instance_of(var_null, TYPE_INT) == false) - assert((var_null is Object) == false) - assert(is_instance_of(var_null, TYPE_OBJECT) == false) - assert((var_null is RefCounted) == false) - assert(is_instance_of(var_null, RefCounted) == false) - assert((var_null is A) == false) - assert(is_instance_of(var_null, A) == false) + Utils.check((var_null is Variant) == true) + Utils.check((var_null is int) == false) + Utils.check(is_instance_of(var_null, TYPE_INT) == false) + Utils.check((var_null is Object) == false) + Utils.check(is_instance_of(var_null, TYPE_OBJECT) == false) + Utils.check((var_null is RefCounted) == false) + Utils.check(is_instance_of(var_null, RefCounted) == false) + Utils.check((var_null is A) == false) + Utils.check(is_instance_of(var_null, A) == false) const const_null: Variant = null - assert((const_null is Variant) == true) - assert((const_null is int) == false) - assert(is_instance_of(const_null, TYPE_INT) == false) - assert((const_null is Object) == false) - assert(is_instance_of(const_null, TYPE_OBJECT) == false) - assert((const_null is RefCounted) == false) - assert(is_instance_of(const_null, RefCounted) == false) - assert((const_null is A) == false) - assert(is_instance_of(const_null, A) == false) + Utils.check((const_null is Variant) == true) + Utils.check((const_null is int) == false) + Utils.check(is_instance_of(const_null, TYPE_INT) == false) + Utils.check((const_null is Object) == false) + Utils.check(is_instance_of(const_null, TYPE_OBJECT) == false) + Utils.check((const_null is RefCounted) == false) + Utils.check(is_instance_of(const_null, RefCounted) == false) + Utils.check((const_null is A) == false) + Utils.check(is_instance_of(const_null, A) == false) print('ok') diff --git a/modules/gdscript/tests/scripts/analyzer/features/typed_array_usage.gd b/modules/gdscript/tests/scripts/analyzer/features/typed_array_usage.gd index b000c82717..fe0274c27b 100644 --- a/modules/gdscript/tests/scripts/analyzer/features/typed_array_usage.gd +++ b/modules/gdscript/tests/scripts/analyzer/features/typed_array_usage.gd @@ -10,206 +10,205 @@ class Members: var two: Array[int] = one func check_passing() -> bool: - assert(str(one) == '[104]') - assert(str(two) == '[104]') + Utils.check(str(one) == '[104]') + Utils.check(str(two) == '[104]') two.push_back(582) - assert(str(one) == '[104, 582]') - assert(str(two) == '[104, 582]') + Utils.check(str(one) == '[104, 582]') + Utils.check(str(two) == '[104, 582]') two = [486] - assert(str(one) == '[104, 582]') - assert(str(two) == '[486]') + Utils.check(str(one) == '[104, 582]') + Utils.check(str(two) == '[486]') return true @warning_ignore("unsafe_method_access") -@warning_ignore("assert_always_true") @warning_ignore("return_value_discarded") func test(): var untyped_basic = [459] - assert(str(untyped_basic) == '[459]') - assert(untyped_basic.get_typed_builtin() == TYPE_NIL) + Utils.check(str(untyped_basic) == '[459]') + Utils.check(untyped_basic.get_typed_builtin() == TYPE_NIL) var inferred_basic := [366] - assert(str(inferred_basic) == '[366]') - assert(inferred_basic.get_typed_builtin() == TYPE_NIL) + Utils.check(str(inferred_basic) == '[366]') + Utils.check(inferred_basic.get_typed_builtin() == TYPE_NIL) var typed_basic: Array = [521] - assert(str(typed_basic) == '[521]') - assert(typed_basic.get_typed_builtin() == TYPE_NIL) + Utils.check(str(typed_basic) == '[521]') + Utils.check(typed_basic.get_typed_builtin() == TYPE_NIL) var empty_floats: Array[float] = [] - assert(str(empty_floats) == '[]') - assert(empty_floats.get_typed_builtin() == TYPE_FLOAT) + Utils.check(str(empty_floats) == '[]') + Utils.check(empty_floats.get_typed_builtin() == TYPE_FLOAT) untyped_basic = empty_floats - assert(untyped_basic.get_typed_builtin() == TYPE_FLOAT) + Utils.check(untyped_basic.get_typed_builtin() == TYPE_FLOAT) inferred_basic = empty_floats - assert(inferred_basic.get_typed_builtin() == TYPE_FLOAT) + Utils.check(inferred_basic.get_typed_builtin() == TYPE_FLOAT) typed_basic = empty_floats - assert(typed_basic.get_typed_builtin() == TYPE_FLOAT) + Utils.check(typed_basic.get_typed_builtin() == TYPE_FLOAT) empty_floats.push_back(705.0) untyped_basic.push_back(430.0) inferred_basic.push_back(263.0) typed_basic.push_back(518.0) - assert(str(empty_floats) == '[705, 430, 263, 518]') - assert(str(untyped_basic) == '[705, 430, 263, 518]') - assert(str(inferred_basic) == '[705, 430, 263, 518]') - assert(str(typed_basic) == '[705, 430, 263, 518]') + Utils.check(str(empty_floats) == '[705, 430, 263, 518]') + Utils.check(str(untyped_basic) == '[705, 430, 263, 518]') + Utils.check(str(inferred_basic) == '[705, 430, 263, 518]') + Utils.check(str(typed_basic) == '[705, 430, 263, 518]') const constant_float := 950.0 const constant_int := 170 var typed_float := 954.0 var filled_floats: Array[float] = [constant_float, constant_int, typed_float, empty_floats[1] + empty_floats[2]] - assert(str(filled_floats) == '[950, 170, 954, 693]') - assert(filled_floats.get_typed_builtin() == TYPE_FLOAT) + Utils.check(str(filled_floats) == '[950, 170, 954, 693]') + Utils.check(filled_floats.get_typed_builtin() == TYPE_FLOAT) var casted_floats := [empty_floats[2] * 2] as Array[float] - assert(str(casted_floats) == '[526]') - assert(casted_floats.get_typed_builtin() == TYPE_FLOAT) + Utils.check(str(casted_floats) == '[526]') + Utils.check(casted_floats.get_typed_builtin() == TYPE_FLOAT) var returned_floats = (func () -> Array[float]: return [554]).call() - assert(str(returned_floats) == '[554]') - assert(returned_floats.get_typed_builtin() == TYPE_FLOAT) + Utils.check(str(returned_floats) == '[554]') + Utils.check(returned_floats.get_typed_builtin() == TYPE_FLOAT) var passed_floats = floats_identity([663.0 if randf() > 0.5 else 663.0]) - assert(str(passed_floats) == '[663]') - assert(passed_floats.get_typed_builtin() == TYPE_FLOAT) + Utils.check(str(passed_floats) == '[663]') + Utils.check(passed_floats.get_typed_builtin() == TYPE_FLOAT) var default_floats = (func (floats: Array[float] = [364.0]): return floats).call() - assert(str(default_floats) == '[364]') - assert(default_floats.get_typed_builtin() == TYPE_FLOAT) + Utils.check(str(default_floats) == '[364]') + Utils.check(default_floats.get_typed_builtin() == TYPE_FLOAT) var typed_int := 556 var converted_floats: Array[float] = [typed_int] converted_floats.push_back(498) - assert(str(converted_floats) == '[556, 498]') - assert(converted_floats.get_typed_builtin() == TYPE_FLOAT) + Utils.check(str(converted_floats) == '[556, 498]') + Utils.check(converted_floats.get_typed_builtin() == TYPE_FLOAT) const constant_basic = [228] - assert(str(constant_basic) == '[228]') - assert(constant_basic.get_typed_builtin() == TYPE_NIL) + Utils.check(str(constant_basic) == '[228]') + Utils.check(constant_basic.get_typed_builtin() == TYPE_NIL) const constant_floats: Array[float] = [constant_float - constant_basic[0] - constant_int] - assert(str(constant_floats) == '[552]') - assert(constant_floats.get_typed_builtin() == TYPE_FLOAT) + Utils.check(str(constant_floats) == '[552]') + Utils.check(constant_floats.get_typed_builtin() == TYPE_FLOAT) var source_floats: Array[float] = [999.74] untyped_basic = source_floats var destination_floats: Array[float] = untyped_basic destination_floats[0] -= 0.74 - assert(str(source_floats) == '[999]') - assert(str(untyped_basic) == '[999]') - assert(str(destination_floats) == '[999]') - assert(destination_floats.get_typed_builtin() == TYPE_FLOAT) + Utils.check(str(source_floats) == '[999]') + Utils.check(str(untyped_basic) == '[999]') + Utils.check(str(destination_floats) == '[999]') + Utils.check(destination_floats.get_typed_builtin() == TYPE_FLOAT) var duplicated_floats := empty_floats.duplicate().slice(2, 3) duplicated_floats[0] *= 3 - assert(str(duplicated_floats) == '[789]') - assert(duplicated_floats.get_typed_builtin() == TYPE_FLOAT) + Utils.check(str(duplicated_floats) == '[789]') + Utils.check(duplicated_floats.get_typed_builtin() == TYPE_FLOAT) var b_objects: Array[B] = [B.new(), B.new() as A, null] - assert(b_objects.size() == 3) - assert(b_objects.get_typed_builtin() == TYPE_OBJECT) - assert(b_objects.get_typed_script() == B) + Utils.check(b_objects.size() == 3) + Utils.check(b_objects.get_typed_builtin() == TYPE_OBJECT) + Utils.check(b_objects.get_typed_script() == B) var a_objects: Array[A] = [A.new(), B.new(), null, b_objects[0]] - assert(a_objects.size() == 4) - assert(a_objects.get_typed_builtin() == TYPE_OBJECT) - assert(a_objects.get_typed_script() == A) + Utils.check(a_objects.size() == 4) + Utils.check(a_objects.get_typed_builtin() == TYPE_OBJECT) + Utils.check(a_objects.get_typed_script() == A) var a_passed = (func check_a_passing(p_objects: Array[A]): return p_objects.size()).call(a_objects) - assert(a_passed == 4) + Utils.check(a_passed == 4) var b_passed = (func check_b_passing(basic: Array): return basic[0] != null).call(b_objects) - assert(b_passed == true) + Utils.check(b_passed == true) var empty_strings: Array[String] = [] var empty_bools: Array[bool] = [] var empty_basic_one := [] var empty_basic_two := [] - assert(empty_strings == empty_bools) - assert(empty_basic_one == empty_basic_two) - assert(empty_strings.hash() == empty_bools.hash()) - assert(empty_basic_one.hash() == empty_basic_two.hash()) + Utils.check(empty_strings == empty_bools) + Utils.check(empty_basic_one == empty_basic_two) + Utils.check(empty_strings.hash() == empty_bools.hash()) + Utils.check(empty_basic_one.hash() == empty_basic_two.hash()) var assign_source: Array[int] = [527] var assign_target: Array[int] = [] assign_target.assign(assign_source) - assert(str(assign_source) == '[527]') - assert(str(assign_target) == '[527]') + Utils.check(str(assign_source) == '[527]') + Utils.check(str(assign_target) == '[527]') assign_source.push_back(657) - assert(str(assign_source) == '[527, 657]') - assert(str(assign_target) == '[527]') + Utils.check(str(assign_source) == '[527, 657]') + Utils.check(str(assign_target) == '[527]') var defaults_passed = (func check_defaults_passing(one: Array[int] = [], two := one): one.push_back(887) two.push_back(198) - assert(str(one) == '[887, 198]') - assert(str(two) == '[887, 198]') + Utils.check(str(one) == '[887, 198]') + Utils.check(str(two) == '[887, 198]') two = [130] - assert(str(one) == '[887, 198]') - assert(str(two) == '[130]') + Utils.check(str(one) == '[887, 198]') + Utils.check(str(two) == '[130]') return true ).call() - assert(defaults_passed == true) + Utils.check(defaults_passed == true) var members := Members.new() var members_passed := members.check_passing() - assert(members_passed == true) + Utils.check(members_passed == true) var resized_basic: Array = [] resized_basic.resize(1) - assert(typeof(resized_basic[0]) == TYPE_NIL) - assert(resized_basic[0] == null) + Utils.check(typeof(resized_basic[0]) == TYPE_NIL) + Utils.check(resized_basic[0] == null) var resized_ints: Array[int] = [] resized_ints.resize(1) - assert(typeof(resized_ints[0]) == TYPE_INT) - assert(resized_ints[0] == 0) + Utils.check(typeof(resized_ints[0]) == TYPE_INT) + Utils.check(resized_ints[0] == 0) var resized_arrays: Array[Array] = [] resized_arrays.resize(1) - assert(typeof(resized_arrays[0]) == TYPE_ARRAY) + Utils.check(typeof(resized_arrays[0]) == TYPE_ARRAY) resized_arrays[0].resize(1) resized_arrays[0][0] = 523 - assert(str(resized_arrays) == '[[523]]') + Utils.check(str(resized_arrays) == '[[523]]') var resized_objects: Array[Object] = [] resized_objects.resize(1) - assert(typeof(resized_objects[0]) == TYPE_NIL) - assert(resized_objects[0] == null) + Utils.check(typeof(resized_objects[0]) == TYPE_NIL) + Utils.check(resized_objects[0] == null) var typed_enums: Array[E] = [] typed_enums.resize(1) - assert(str(typed_enums) == '[0]') + Utils.check(str(typed_enums) == '[0]') typed_enums[0] = E.E0 - assert(str(typed_enums) == '[391]') - assert(typed_enums.get_typed_builtin() == TYPE_INT) + Utils.check(str(typed_enums) == '[391]') + Utils.check(typed_enums.get_typed_builtin() == TYPE_INT) const const_enums: Array[E] = [] - assert(const_enums.get_typed_builtin() == TYPE_INT) - assert(const_enums.get_typed_class_name() == &'') + Utils.check(const_enums.get_typed_builtin() == TYPE_INT) + Utils.check(const_enums.get_typed_class_name() == &'') var a := A.new() var typed_natives: Array[RefCounted] = [a] var typed_scripts = Array(typed_natives, TYPE_OBJECT, "RefCounted", A) - assert(typed_scripts[0] == a) + Utils.check(typed_scripts[0] == a) print('ok') diff --git a/modules/gdscript/tests/scripts/analyzer/warnings/non_tool_extends_tool.gd b/modules/gdscript/tests/scripts/analyzer/warnings/non_tool_extends_tool.gd new file mode 100644 index 0000000000..95d497c3f3 --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/warnings/non_tool_extends_tool.gd @@ -0,0 +1,7 @@ +extends "./non_tool_extends_tool.notest.gd" + +class InnerClass extends "./non_tool_extends_tool.notest.gd": + pass + +func test(): + pass diff --git a/modules/gdscript/tests/scripts/analyzer/warnings/non_tool_extends_tool.notest.gd b/modules/gdscript/tests/scripts/analyzer/warnings/non_tool_extends_tool.notest.gd new file mode 100644 index 0000000000..07427846d1 --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/warnings/non_tool_extends_tool.notest.gd @@ -0,0 +1 @@ +@tool diff --git a/modules/gdscript/tests/scripts/analyzer/warnings/non_tool_extends_tool.out b/modules/gdscript/tests/scripts/analyzer/warnings/non_tool_extends_tool.out new file mode 100644 index 0000000000..f65caf5222 --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/warnings/non_tool_extends_tool.out @@ -0,0 +1,9 @@ +GDTEST_OK +>> WARNING +>> Line: 1 +>> MISSING_TOOL +>> The base class script has the "@tool" annotation, but this script does not have it. +>> WARNING +>> Line: 3 +>> MISSING_TOOL +>> The base class script has the "@tool" annotation, but this script does not have it. diff --git a/modules/gdscript/tests/scripts/analyzer/warnings/non_tool_extends_tool_ignored.gd b/modules/gdscript/tests/scripts/analyzer/warnings/non_tool_extends_tool_ignored.gd new file mode 100644 index 0000000000..a452307d99 --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/warnings/non_tool_extends_tool_ignored.gd @@ -0,0 +1,9 @@ +@warning_ignore("missing_tool") +extends "./non_tool_extends_tool.notest.gd" + +@warning_ignore("missing_tool") +class InnerClass extends "./non_tool_extends_tool.notest.gd": + pass + +func test(): + pass diff --git a/modules/gdscript/tests/scripts/analyzer/warnings/non_tool_extends_tool_ignored.out b/modules/gdscript/tests/scripts/analyzer/warnings/non_tool_extends_tool_ignored.out new file mode 100644 index 0000000000..d73c5eb7cd --- /dev/null +++ b/modules/gdscript/tests/scripts/analyzer/warnings/non_tool_extends_tool_ignored.out @@ -0,0 +1 @@ +GDTEST_OK diff --git a/modules/gdscript/tests/scripts/completion/index/array_type.cfg b/modules/gdscript/tests/scripts/completion/index/array_type.cfg new file mode 100644 index 0000000000..5cd5565d00 --- /dev/null +++ b/modules/gdscript/tests/scripts/completion/index/array_type.cfg @@ -0,0 +1,9 @@ +[output] +include=[ + {"display": "outer"}, + {"display": "inner"}, +] +exclude=[ + {"display": "append"}, + {"display": "\"append\""}, +] diff --git a/modules/gdscript/tests/scripts/completion/index/array_type.gd b/modules/gdscript/tests/scripts/completion/index/array_type.gd new file mode 100644 index 0000000000..e0a15da556 --- /dev/null +++ b/modules/gdscript/tests/scripts/completion/index/array_type.gd @@ -0,0 +1,10 @@ +extends Node + +var outer + +func _ready() -> void: + var inner + + var array: Array + + array[i➡] diff --git a/modules/gdscript/tests/scripts/completion/index/array_value.cfg b/modules/gdscript/tests/scripts/completion/index/array_value.cfg new file mode 100644 index 0000000000..5cd5565d00 --- /dev/null +++ b/modules/gdscript/tests/scripts/completion/index/array_value.cfg @@ -0,0 +1,9 @@ +[output] +include=[ + {"display": "outer"}, + {"display": "inner"}, +] +exclude=[ + {"display": "append"}, + {"display": "\"append\""}, +] diff --git a/modules/gdscript/tests/scripts/completion/index/array_value.gd b/modules/gdscript/tests/scripts/completion/index/array_value.gd new file mode 100644 index 0000000000..17451725bc --- /dev/null +++ b/modules/gdscript/tests/scripts/completion/index/array_value.gd @@ -0,0 +1,10 @@ +extends Node + +var outer + +func _ready() -> void: + var inner + + var array = [] + + array[i➡] diff --git a/modules/gdscript/tests/scripts/completion/index/const_dictionary_keys.cfg b/modules/gdscript/tests/scripts/completion/index/const_dictionary_keys.cfg new file mode 100644 index 0000000000..ecea284b5d --- /dev/null +++ b/modules/gdscript/tests/scripts/completion/index/const_dictionary_keys.cfg @@ -0,0 +1,11 @@ +[output] +include=[ + {"display": "\"key1\""}, + {"display": "\"key2\""}, +] +exclude=[ + {"display": "keys"}, + {"display": "\"keys\""}, + {"display": "key1"}, + {"display": "key2"}, +] diff --git a/modules/gdscript/tests/scripts/completion/index/const_dictionary_keys.gd b/modules/gdscript/tests/scripts/completion/index/const_dictionary_keys.gd new file mode 100644 index 0000000000..06498c57a6 --- /dev/null +++ b/modules/gdscript/tests/scripts/completion/index/const_dictionary_keys.gd @@ -0,0 +1,13 @@ +extends Node + +var outer + +const dict = { + "key1": "value", + "key2": null, +} + +func _ready() -> void: + var inner + + dict["➡"] diff --git a/modules/gdscript/tests/scripts/completion/index/dictionary_type.cfg b/modules/gdscript/tests/scripts/completion/index/dictionary_type.cfg new file mode 100644 index 0000000000..cddf7b8cc8 --- /dev/null +++ b/modules/gdscript/tests/scripts/completion/index/dictionary_type.cfg @@ -0,0 +1,9 @@ +[output] +include=[ + {"display": "outer"}, + {"display": "inner"}, +] +exclude=[ + {"display": "keys"}, + {"display": "\"keys\""}, +] diff --git a/modules/gdscript/tests/scripts/completion/index/dictionary_type.gd b/modules/gdscript/tests/scripts/completion/index/dictionary_type.gd new file mode 100644 index 0000000000..b02c62eea5 --- /dev/null +++ b/modules/gdscript/tests/scripts/completion/index/dictionary_type.gd @@ -0,0 +1,10 @@ +extends Node + +var outer + +func _ready() -> void: + var inner + + var dict: Dictionary + + dict[i➡] diff --git a/modules/gdscript/tests/scripts/completion/index/dictionary_value.cfg b/modules/gdscript/tests/scripts/completion/index/dictionary_value.cfg new file mode 100644 index 0000000000..cddf7b8cc8 --- /dev/null +++ b/modules/gdscript/tests/scripts/completion/index/dictionary_value.cfg @@ -0,0 +1,9 @@ +[output] +include=[ + {"display": "outer"}, + {"display": "inner"}, +] +exclude=[ + {"display": "keys"}, + {"display": "\"keys\""}, +] diff --git a/modules/gdscript/tests/scripts/completion/index/dictionary_value.gd b/modules/gdscript/tests/scripts/completion/index/dictionary_value.gd new file mode 100644 index 0000000000..60bf391716 --- /dev/null +++ b/modules/gdscript/tests/scripts/completion/index/dictionary_value.gd @@ -0,0 +1,10 @@ +extends Node + +var outer + +func _ready() -> void: + var inner + + var dict = {} + + dict[i➡] diff --git a/modules/gdscript/tests/scripts/completion/index/local_dictionary_keys.cfg b/modules/gdscript/tests/scripts/completion/index/local_dictionary_keys.cfg new file mode 100644 index 0000000000..ecea284b5d --- /dev/null +++ b/modules/gdscript/tests/scripts/completion/index/local_dictionary_keys.cfg @@ -0,0 +1,11 @@ +[output] +include=[ + {"display": "\"key1\""}, + {"display": "\"key2\""}, +] +exclude=[ + {"display": "keys"}, + {"display": "\"keys\""}, + {"display": "key1"}, + {"display": "key2"}, +] diff --git a/modules/gdscript/tests/scripts/completion/index/local_dictionary_keys.gd b/modules/gdscript/tests/scripts/completion/index/local_dictionary_keys.gd new file mode 100644 index 0000000000..2220cdcc59 --- /dev/null +++ b/modules/gdscript/tests/scripts/completion/index/local_dictionary_keys.gd @@ -0,0 +1,13 @@ +extends Node + +var outer + +func _ready() -> void: + var inner + + var dict: Dictionary = { + "key1": "value", + "key2": null, + } + + dict["➡"] diff --git a/modules/gdscript/tests/scripts/completion/index/property_dictionary_keys.cfg b/modules/gdscript/tests/scripts/completion/index/property_dictionary_keys.cfg new file mode 100644 index 0000000000..8da525bff8 --- /dev/null +++ b/modules/gdscript/tests/scripts/completion/index/property_dictionary_keys.cfg @@ -0,0 +1,9 @@ +[output] +exclude=[ + {"display": "keys"}, + {"display": "\"keys\""}, + {"display": "key1"}, + {"display": "key2"}, + {"display": "\"key1\""}, + {"display": "\"key2\""}, +] diff --git a/modules/gdscript/tests/scripts/completion/index/property_dictionary_keys.gd b/modules/gdscript/tests/scripts/completion/index/property_dictionary_keys.gd new file mode 100644 index 0000000000..ba8d7f76fd --- /dev/null +++ b/modules/gdscript/tests/scripts/completion/index/property_dictionary_keys.gd @@ -0,0 +1,13 @@ +extends Node + +var outer + +var dict = { + "key1": "value", + "key2": null, +} + +func _ready() -> void: + var inner + + dict["➡"] diff --git a/modules/gdscript/tests/scripts/completion/index/untyped_local.cfg b/modules/gdscript/tests/scripts/completion/index/untyped_local.cfg new file mode 100644 index 0000000000..1173043f94 --- /dev/null +++ b/modules/gdscript/tests/scripts/completion/index/untyped_local.cfg @@ -0,0 +1,5 @@ +[output] +include=[ + {"display": "outer"}, + {"display": "inner"}, +] diff --git a/modules/gdscript/tests/scripts/completion/index/untyped_local.gd b/modules/gdscript/tests/scripts/completion/index/untyped_local.gd new file mode 100644 index 0000000000..1a1157af02 --- /dev/null +++ b/modules/gdscript/tests/scripts/completion/index/untyped_local.gd @@ -0,0 +1,10 @@ +extends Node + +var outer + +func _ready() -> void: + var inner + + var array + + array[i➡] diff --git a/modules/gdscript/tests/scripts/completion/index/untyped_property.cfg b/modules/gdscript/tests/scripts/completion/index/untyped_property.cfg new file mode 100644 index 0000000000..1173043f94 --- /dev/null +++ b/modules/gdscript/tests/scripts/completion/index/untyped_property.cfg @@ -0,0 +1,5 @@ +[output] +include=[ + {"display": "outer"}, + {"display": "inner"}, +] diff --git a/modules/gdscript/tests/scripts/completion/index/untyped_property.gd b/modules/gdscript/tests/scripts/completion/index/untyped_property.gd new file mode 100644 index 0000000000..9fa23da504 --- /dev/null +++ b/modules/gdscript/tests/scripts/completion/index/untyped_property.gd @@ -0,0 +1,9 @@ +extends Node + +var outer +var array + +func _ready() -> void: + var inner + + array[i➡] diff --git a/modules/gdscript/tests/scripts/parser/features/annotations.gd b/modules/gdscript/tests/scripts/parser/features/annotations.gd index 7a7d6d953e..1e5d3fdcad 100644 --- a/modules/gdscript/tests/scripts/parser/features/annotations.gd +++ b/modules/gdscript/tests/scripts/parser/features/annotations.gd @@ -1,7 +1,5 @@ extends Node -const Utils = preload("../../utils.notest.gd") - @export_enum("A", "B", "C") var test_1 @export_enum("A", "B", "C",) var test_2 diff --git a/modules/gdscript/tests/scripts/parser/features/class.gd b/modules/gdscript/tests/scripts/parser/features/class.gd index af24b32322..482d04a63b 100644 --- a/modules/gdscript/tests/scripts/parser/features/class.gd +++ b/modules/gdscript/tests/scripts/parser/features/class.gd @@ -18,8 +18,8 @@ func test(): test_instance.number = 42 var test_sub = TestSub.new() - assert(test_sub.number == 25) # From Test. - assert(test_sub.other_string == "bye") # From TestSub. + Utils.check(test_sub.number == 25) # From Test. + Utils.check(test_sub.other_string == "bye") # From TestSub. var _test_constructor = TestConstructor.new() _test_constructor = TestConstructor.new(500) diff --git a/modules/gdscript/tests/scripts/parser/features/export_arrays.gd b/modules/gdscript/tests/scripts/parser/features/export_arrays.gd index 0d97135a7b..cfda255905 100644 --- a/modules/gdscript/tests/scripts/parser/features/export_arrays.gd +++ b/modules/gdscript/tests/scripts/parser/features/export_arrays.gd @@ -1,5 +1,3 @@ -const Utils = preload("../../utils.notest.gd") - @export_dir var test_dir: Array[String] @export_dir var test_dir_packed: PackedStringArray @export_file var test_file: Array[String] diff --git a/modules/gdscript/tests/scripts/parser/features/export_enum.gd b/modules/gdscript/tests/scripts/parser/features/export_enum.gd index 7f0737f4db..d50f0b2528 100644 --- a/modules/gdscript/tests/scripts/parser/features/export_enum.gd +++ b/modules/gdscript/tests/scripts/parser/features/export_enum.gd @@ -1,5 +1,3 @@ -const Utils = preload("../../utils.notest.gd") - @export_enum("Red", "Green", "Blue") var test_untyped @export_enum("Red:10", "Green:20", "Blue:30") var test_with_values diff --git a/modules/gdscript/tests/scripts/parser/features/export_variable.gd b/modules/gdscript/tests/scripts/parser/features/export_variable.gd index 8bcb2bcb9a..1e134d0e0e 100644 --- a/modules/gdscript/tests/scripts/parser/features/export_variable.gd +++ b/modules/gdscript/tests/scripts/parser/features/export_variable.gd @@ -1,7 +1,6 @@ class_name ExportVariableTest extends Node -const Utils = preload("../../utils.notest.gd") const PreloadedGlobalClass = preload("./export_variable_global.notest.gd") const PreloadedUnnamedClass = preload("./export_variable_unnamed.notest.gd") diff --git a/modules/gdscript/tests/scripts/parser/features/good_continue_in_lambda.gd b/modules/gdscript/tests/scripts/parser/features/good_continue_in_lambda.gd index 2fa45c1d7d..0ec118b6b7 100644 --- a/modules/gdscript/tests/scripts/parser/features/good_continue_in_lambda.gd +++ b/modules/gdscript/tests/scripts/parser/features/good_continue_in_lambda.gd @@ -9,5 +9,5 @@ func test(): j_string += str(j) return j_string i_string += lambda.call() - assert(i_string == '0202') + Utils.check(i_string == '0202') print('ok') diff --git a/modules/gdscript/tests/scripts/parser/features/truthiness.gd b/modules/gdscript/tests/scripts/parser/features/truthiness.gd index 9c67a152f5..736cda7f74 100644 --- a/modules/gdscript/tests/scripts/parser/features/truthiness.gd +++ b/modules/gdscript/tests/scripts/parser/features/truthiness.gd @@ -1,30 +1,32 @@ func test(): - # The assertions below should all evaluate to `true` for this test to pass. - assert(true) - assert(not false) - assert(500) - assert(not 0) - assert(500.5) - assert(not 0.0) - assert("non-empty string") - assert(["non-empty array"]) - assert({"non-empty": "dictionary"}) - assert(Vector2(1, 0)) - assert(Vector2i(-1, -1)) - assert(Vector3(0, 0, 0.0001)) - assert(Vector3i(0, 0, 10000)) + # The checks below should all evaluate to `true` for this test to pass. + Utils.check(true) + Utils.check(not false) + Utils.check(500) + Utils.check(not 0) + Utils.check(500.5) + Utils.check(not 0.0) + Utils.check("non-empty string") + Utils.check(["non-empty array"]) + Utils.check({"non-empty": "dictionary"}) + Utils.check(Vector2(1, 0)) + Utils.check(Vector2i(-1, -1)) + Utils.check(Vector3(0, 0, 0.0001)) + Utils.check(Vector3i(0, 0, 10000)) # Zero position is `true` only if the Rect2's size is non-zero. - assert(Rect2(0, 0, 0, 1)) + Utils.check(Rect2(0, 0, 0, 1)) # Zero size is `true` only if the position is non-zero. - assert(Rect2(1, 1, 0, 0)) + Utils.check(Rect2(1, 1, 0, 0)) # Zero position is `true` only if the Rect2's size is non-zero. - assert(Rect2i(0, 0, 0, 1)) + Utils.check(Rect2i(0, 0, 0, 1)) # Zero size is `true` only if the position is non-zero. - assert(Rect2i(1, 1, 0, 0)) + Utils.check(Rect2i(1, 1, 0, 0)) # A fully black color is only truthy if its alpha component is not equal to `1`. - assert(Color(0, 0, 0, 0.5)) + Utils.check(Color(0, 0, 0, 0.5)) + + print("ok") diff --git a/modules/gdscript/tests/scripts/parser/features/truthiness.out b/modules/gdscript/tests/scripts/parser/features/truthiness.out index 705524857b..1b47ed10dc 100644 --- a/modules/gdscript/tests/scripts/parser/features/truthiness.out +++ b/modules/gdscript/tests/scripts/parser/features/truthiness.out @@ -1,65 +1,2 @@ GDTEST_OK ->> WARNING ->> Line: 3 ->> ASSERT_ALWAYS_TRUE ->> Assert statement is redundant because the expression is always true. ->> WARNING ->> Line: 4 ->> ASSERT_ALWAYS_TRUE ->> Assert statement is redundant because the expression is always true. ->> WARNING ->> Line: 5 ->> ASSERT_ALWAYS_TRUE ->> Assert statement is redundant because the expression is always true. ->> WARNING ->> Line: 6 ->> ASSERT_ALWAYS_TRUE ->> Assert statement is redundant because the expression is always true. ->> WARNING ->> Line: 7 ->> ASSERT_ALWAYS_TRUE ->> Assert statement is redundant because the expression is always true. ->> WARNING ->> Line: 8 ->> ASSERT_ALWAYS_TRUE ->> Assert statement is redundant because the expression is always true. ->> WARNING ->> Line: 9 ->> ASSERT_ALWAYS_TRUE ->> Assert statement is redundant because the expression is always true. ->> WARNING ->> Line: 12 ->> ASSERT_ALWAYS_TRUE ->> Assert statement is redundant because the expression is always true. ->> WARNING ->> Line: 13 ->> ASSERT_ALWAYS_TRUE ->> Assert statement is redundant because the expression is always true. ->> WARNING ->> Line: 14 ->> ASSERT_ALWAYS_TRUE ->> Assert statement is redundant because the expression is always true. ->> WARNING ->> Line: 15 ->> ASSERT_ALWAYS_TRUE ->> Assert statement is redundant because the expression is always true. ->> WARNING ->> Line: 18 ->> ASSERT_ALWAYS_TRUE ->> Assert statement is redundant because the expression is always true. ->> WARNING ->> Line: 21 ->> ASSERT_ALWAYS_TRUE ->> Assert statement is redundant because the expression is always true. ->> WARNING ->> Line: 24 ->> ASSERT_ALWAYS_TRUE ->> Assert statement is redundant because the expression is always true. ->> WARNING ->> Line: 27 ->> ASSERT_ALWAYS_TRUE ->> Assert statement is redundant because the expression is always true. ->> WARNING ->> Line: 30 ->> ASSERT_ALWAYS_TRUE ->> Assert statement is redundant because the expression is always true. +ok diff --git a/modules/gdscript/tests/scripts/runtime/features/array_string_stringname_equivalent.gd b/modules/gdscript/tests/scripts/runtime/features/array_string_stringname_equivalent.gd index bd38259cec..6eec37d64d 100644 --- a/modules/gdscript/tests/scripts/runtime/features/array_string_stringname_equivalent.gd +++ b/modules/gdscript/tests/scripts/runtime/features/array_string_stringname_equivalent.gd @@ -25,7 +25,7 @@ func test(): print("String in Array[StringName]: ", "abc" in stringname_array) var packed_string_array: PackedStringArray = [] - assert(!packed_string_array.push_back("abc")) + Utils.check(!packed_string_array.push_back("abc")) print("StringName in PackedStringArray: ", &"abc" in packed_string_array) string_array.push_back("abc") diff --git a/modules/gdscript/tests/scripts/runtime/features/constants_are_read_only.gd b/modules/gdscript/tests/scripts/runtime/features/constants_are_read_only.gd index d1746979be..6aa863c05f 100644 --- a/modules/gdscript/tests/scripts/runtime/features/constants_are_read_only.gd +++ b/modules/gdscript/tests/scripts/runtime/features/constants_are_read_only.gd @@ -1,10 +1,9 @@ const array: Array = [0] const dictionary := {1: 2} -@warning_ignore("assert_always_true") func test(): - assert(array.is_read_only() == true) - assert(str(array) == '[0]') - assert(dictionary.is_read_only() == true) - assert(str(dictionary) == '{ 1: 2 }') + Utils.check(array.is_read_only() == true) + Utils.check(str(array) == '[0]') + Utils.check(dictionary.is_read_only() == true) + Utils.check(str(dictionary) == '{ 1: 2 }') print('ok') diff --git a/modules/gdscript/tests/scripts/runtime/features/conversions_from_native_members.gd b/modules/gdscript/tests/scripts/runtime/features/conversions_from_native_members.gd index a778fb1a94..0f2526667d 100644 --- a/modules/gdscript/tests/scripts/runtime/features/conversions_from_native_members.gd +++ b/modules/gdscript/tests/scripts/runtime/features/conversions_from_native_members.gd @@ -2,8 +2,8 @@ class Foo extends Node: func _init(): name = 'f' var string: String = name - assert(typeof(string) == TYPE_STRING) - assert(string == 'f') + Utils.check(typeof(string) == TYPE_STRING) + Utils.check(string == 'f') print('ok') func test(): diff --git a/modules/gdscript/tests/scripts/runtime/features/default_set_beforehand.gd b/modules/gdscript/tests/scripts/runtime/features/default_set_beforehand.gd index 0851d939dc..9e67e75140 100644 --- a/modules/gdscript/tests/scripts/runtime/features/default_set_beforehand.gd +++ b/modules/gdscript/tests/scripts/runtime/features/default_set_beforehand.gd @@ -6,15 +6,15 @@ extends Node @onready var later_untyped = [1] func test(): - assert(typeof(later_inferred) == TYPE_ARRAY) - assert(later_inferred.size() == 0) + Utils.check(typeof(later_inferred) == TYPE_ARRAY) + Utils.check(later_inferred.size() == 0) - assert(typeof(later_static) == TYPE_ARRAY) - assert(later_static.size() == 0) + Utils.check(typeof(later_static) == TYPE_ARRAY) + Utils.check(later_static.size() == 0) - assert(typeof(later_static_with_init) == TYPE_ARRAY) - assert(later_static_with_init.size() == 0) + Utils.check(typeof(later_static_with_init) == TYPE_ARRAY) + Utils.check(later_static_with_init.size() == 0) - assert(typeof(later_untyped) == TYPE_NIL) + Utils.check(typeof(later_untyped) == TYPE_NIL) print("ok") diff --git a/modules/gdscript/tests/scripts/runtime/features/export_group_no_name_conflict_with_properties.gd b/modules/gdscript/tests/scripts/runtime/features/export_group_no_name_conflict_with_properties.gd index 0133d7fcfc..90df98e05b 100644 --- a/modules/gdscript/tests/scripts/runtime/features/export_group_no_name_conflict_with_properties.gd +++ b/modules/gdscript/tests/scripts/runtime/features/export_group_no_name_conflict_with_properties.gd @@ -1,5 +1,3 @@ -const Utils = preload("../../utils.notest.gd") - # GH-73843 @export_group("Resource") diff --git a/modules/gdscript/tests/scripts/runtime/features/match_with_pattern_guards.gd b/modules/gdscript/tests/scripts/runtime/features/match_with_pattern_guards.gd index 4cb51f8512..48a9349bf8 100644 --- a/modules/gdscript/tests/scripts/runtime/features/match_with_pattern_guards.gd +++ b/modules/gdscript/tests/scripts/runtime/features/match_with_pattern_guards.gd @@ -62,7 +62,7 @@ func test(): 0 when side_effect(): print("will run the side effect call, but not this") _: - assert(global == 1) + Utils.check(global == 1) print("side effect only ran once") func side_effect(): diff --git a/modules/gdscript/tests/scripts/runtime/features/member_info.gd b/modules/gdscript/tests/scripts/runtime/features/member_info.gd index 42b29eee43..91d5a501c8 100644 --- a/modules/gdscript/tests/scripts/runtime/features/member_info.gd +++ b/modules/gdscript/tests/scripts/runtime/features/member_info.gd @@ -5,8 +5,6 @@ class MyClass: enum MyEnum {} -const Utils = preload("../../utils.notest.gd") - static var test_static_var_untyped static var test_static_var_weak_null = null static var test_static_var_weak_int = 1 diff --git a/modules/gdscript/tests/scripts/runtime/features/member_info_inheritance.gd b/modules/gdscript/tests/scripts/runtime/features/member_info_inheritance.gd index ee5c1e1267..4ddbeaec0b 100644 --- a/modules/gdscript/tests/scripts/runtime/features/member_info_inheritance.gd +++ b/modules/gdscript/tests/scripts/runtime/features/member_info_inheritance.gd @@ -1,7 +1,5 @@ # GH-82169 -const Utils = preload("../../utils.notest.gd") - class A: static var test_static_var_a1 static var test_static_var_a2 diff --git a/modules/gdscript/tests/scripts/runtime/features/metatypes.gd b/modules/gdscript/tests/scripts/runtime/features/metatypes.gd index fd23ea0db5..d6847768e6 100644 --- a/modules/gdscript/tests/scripts/runtime/features/metatypes.gd +++ b/modules/gdscript/tests/scripts/runtime/features/metatypes.gd @@ -3,7 +3,6 @@ class MyClass: enum MyEnum {A, B, C} -const Utils = preload("../../utils.notest.gd") const Other = preload("./metatypes.notest.gd") var test_native := JSON diff --git a/modules/gdscript/tests/scripts/runtime/features/set_does_not_leak.gd b/modules/gdscript/tests/scripts/runtime/features/set_does_not_leak.gd index e1aba83507..dee36d3ae0 100644 --- a/modules/gdscript/tests/scripts/runtime/features/set_does_not_leak.gd +++ b/modules/gdscript/tests/scripts/runtime/features/set_does_not_leak.gd @@ -6,6 +6,6 @@ class MyObj: func test(): var obj_1 = MyObj.new() var obj_2 = MyObj.new() - assert(obj_2.get_reference_count() == 1) + Utils.check(obj_2.get_reference_count() == 1) obj_1.set(&"obj", obj_2) - assert(obj_2.get_reference_count() == 1) + Utils.check(obj_2.get_reference_count() == 1) diff --git a/modules/gdscript/tests/scripts/runtime/features/single_underscore_node_name.gd b/modules/gdscript/tests/scripts/runtime/features/single_underscore_node_name.gd index b07c40b6da..11a670a7fb 100644 --- a/modules/gdscript/tests/scripts/runtime/features/single_underscore_node_name.gd +++ b/modules/gdscript/tests/scripts/runtime/features/single_underscore_node_name.gd @@ -12,4 +12,4 @@ func test() -> void: node1.add_child(node2) add_child(node3) - assert(get_node("_/Child") == $_/Child) + Utils.check(get_node("_/Child") == $_/Child) diff --git a/modules/gdscript/tests/scripts/runtime/features/standalone_calls_do_not_write_to_nil.gd b/modules/gdscript/tests/scripts/runtime/features/standalone_calls_do_not_write_to_nil.gd index 691b611574..d72662736e 100644 --- a/modules/gdscript/tests/scripts/runtime/features/standalone_calls_do_not_write_to_nil.gd +++ b/modules/gdscript/tests/scripts/runtime/features/standalone_calls_do_not_write_to_nil.gd @@ -14,33 +14,33 @@ func test(): func test_construct(v, f): @warning_ignore("unsafe_call_argument") Vector2(v, v) # Built-in type construct. - assert(not f) # Test unary operator reading from `nil`. + Utils.check(not f) # Test unary operator reading from `nil`. func test_utility(v, f): abs(v) # Utility function. - assert(not f) # Test unary operator reading from `nil`. + Utils.check(not f) # Test unary operator reading from `nil`. func test_builtin_call(v, f): @warning_ignore("unsafe_method_access") v.angle() # Built-in method call. - assert(not f) # Test unary operator reading from `nil`. + Utils.check(not f) # Test unary operator reading from `nil`. func test_builtin_call_validated(v: Vector2, f): @warning_ignore("return_value_discarded") v.abs() # Built-in method call validated. - assert(not f) # Test unary operator reading from `nil`. + Utils.check(not f) # Test unary operator reading from `nil`. func test_object_call(v, f): @warning_ignore("unsafe_method_access") v.get_reference_count() # Native type method call. - assert(not f) # Test unary operator reading from `nil`. + Utils.check(not f) # Test unary operator reading from `nil`. func test_object_call_method_bind(v: Resource, f): @warning_ignore("return_value_discarded") v.duplicate() # Native type method call with MethodBind. - assert(not f) # Test unary operator reading from `nil`. + Utils.check(not f) # Test unary operator reading from `nil`. func test_object_call_method_bind_validated(v: RefCounted, f): @warning_ignore("return_value_discarded") v.get_reference_count() # Native type method call with validated MethodBind. - assert(not f) # Test unary operator reading from `nil`. + Utils.check(not f) # Test unary operator reading from `nil`. diff --git a/modules/gdscript/tests/scripts/runtime/features/typed_array_init_with_untyped_in_literal.gd b/modules/gdscript/tests/scripts/runtime/features/typed_array_init_with_untyped_in_literal.gd index ec444b4ffa..859bfd7987 100644 --- a/modules/gdscript/tests/scripts/runtime/features/typed_array_init_with_untyped_in_literal.gd +++ b/modules/gdscript/tests/scripts/runtime/features/typed_array_init_with_untyped_in_literal.gd @@ -1,6 +1,6 @@ func test(): var untyped: Variant = 32 var typed: Array[int] = [untyped] - assert(typed.get_typed_builtin() == TYPE_INT) - assert(str(typed) == '[32]') + Utils.check(typed.get_typed_builtin() == TYPE_INT) + Utils.check(str(typed) == '[32]') print('ok') diff --git a/modules/gdscript/tests/scripts/utils.notest.gd b/modules/gdscript/tests/scripts/utils.notest.gd index 7fdd6556ec..5d615d8557 100644 --- a/modules/gdscript/tests/scripts/utils.notest.gd +++ b/modules/gdscript/tests/scripts/utils.notest.gd @@ -1,3 +1,13 @@ +class_name Utils + + +# `assert()` is not evaluated in non-debug builds. Do not use `assert()` +# for anything other than testing the `assert()` itself. +static func check(condition: Variant) -> void: + if not condition: + printerr("Check failed.") + + static func get_type(property: Dictionary, is_return: bool = false) -> String: match property.type: TYPE_NIL: @@ -46,7 +56,7 @@ static func get_human_readable_hint_string(property: Dictionary) -> String: while true: if not hint_string.contains(":"): - push_error("Invalid PROPERTY_HINT_TYPE_STRING format.") + printerr("Invalid PROPERTY_HINT_TYPE_STRING format.") var elem_type_hint: String = hint_string.get_slice(":", 0) hint_string = hint_string.substr(elem_type_hint.length() + 1) @@ -58,7 +68,7 @@ static func get_human_readable_hint_string(property: Dictionary) -> String: type_hint_prefixes += "<%s>:" % type_string(elem_type) else: if elem_type_hint.count("/") != 1: - push_error("Invalid PROPERTY_HINT_TYPE_STRING format.") + printerr("Invalid PROPERTY_HINT_TYPE_STRING format.") elem_type = elem_type_hint.get_slice("/", 0).to_int() elem_hint = elem_type_hint.get_slice("/", 1).to_int() type_hint_prefixes += "<%s>/<%s>:" % [ @@ -188,7 +198,7 @@ static func get_property_hint_name(hint: PropertyHint) -> String: return "PROPERTY_HINT_HIDE_QUATERNION_EDIT" PROPERTY_HINT_PASSWORD: return "PROPERTY_HINT_PASSWORD" - push_error("Argument `hint` is invalid. Use `PROPERTY_HINT_*` constants.") + printerr("Argument `hint` is invalid. Use `PROPERTY_HINT_*` constants.") return "<invalid hint>" @@ -240,7 +250,7 @@ static func get_property_usage_string(usage: int) -> String: usage &= ~flag[0] if usage != PROPERTY_USAGE_NONE: - push_error("Argument `usage` is invalid. Use `PROPERTY_USAGE_*` constants.") + printerr("Argument `usage` is invalid. Use `PROPERTY_USAGE_*` constants.") return "<invalid usage flags>" return result.left(-1) diff --git a/modules/gltf/doc_classes/GLTFDocument.xml b/modules/gltf/doc_classes/GLTFDocument.xml index ebeed015e9..10534594d3 100644 --- a/modules/gltf/doc_classes/GLTFDocument.xml +++ b/modules/gltf/doc_classes/GLTFDocument.xml @@ -63,6 +63,13 @@ The [param bake_fps] parameter overrides the bake_fps in [param state]. </description> </method> + <method name="get_supported_gltf_extensions" qualifiers="static"> + <return type="PackedStringArray" /> + <description> + Returns a list of all support glTF extensions, including extensions supported directly by the engine, and extensions supported by user plugins registering [GLTFDocumentExtension] classes. + [b]Note:[/b] If this method is run before a GLTFDocumentExtension is registered, its extensions won't be included in the list. Be sure to only run this method after all extensions are registered. If you run this when the engine starts, consider waiting a frame before calling this method to ensure all extensions are registered. + </description> + </method> <method name="register_gltf_document_extension" qualifiers="static"> <return type="void" /> <param index="0" name="extension" type="GLTFDocumentExtension" /> diff --git a/modules/gltf/editor/editor_scene_importer_blend.cpp b/modules/gltf/editor/editor_scene_importer_blend.cpp index 0aab6e84b4..8e5a992bd4 100644 --- a/modules/gltf/editor/editor_scene_importer_blend.cpp +++ b/modules/gltf/editor/editor_scene_importer_blend.cpp @@ -336,7 +336,7 @@ Node *EditorSceneFormatImporterBlend::import_scene(const String &p_path, uint32_ #endif } -Variant EditorSceneFormatImporterBlend::get_option_visibility(const String &p_path, bool p_for_animation, const String &p_option, +Variant EditorSceneFormatImporterBlend::get_option_visibility(const String &p_path, const String &p_scene_import_type, const String &p_option, const HashMap<StringName, Variant> &p_options) { if (p_path.get_extension().to_lower() != "blend") { return true; diff --git a/modules/gltf/editor/editor_scene_importer_blend.h b/modules/gltf/editor/editor_scene_importer_blend.h index 6adace9276..17eb9e5709 100644 --- a/modules/gltf/editor/editor_scene_importer_blend.h +++ b/modules/gltf/editor/editor_scene_importer_blend.h @@ -74,7 +74,7 @@ public: List<String> *r_missing_deps, Error *r_err = nullptr) override; virtual void get_import_options(const String &p_path, List<ResourceImporter::ImportOption> *r_options) override; - virtual Variant get_option_visibility(const String &p_path, bool p_for_animation, const String &p_option, + virtual Variant get_option_visibility(const String &p_path, const String &p_scene_import_type, const String &p_option, const HashMap<StringName, Variant> &p_options) override; }; diff --git a/modules/gltf/editor/editor_scene_importer_gltf.cpp b/modules/gltf/editor/editor_scene_importer_gltf.cpp index ce7e17d361..41e294cfc6 100644 --- a/modules/gltf/editor/editor_scene_importer_gltf.cpp +++ b/modules/gltf/editor/editor_scene_importer_gltf.cpp @@ -100,7 +100,7 @@ void EditorSceneFormatImporterGLTF::handle_compatibility_options(HashMap<StringN } } -Variant EditorSceneFormatImporterGLTF::get_option_visibility(const String &p_path, bool p_for_animation, +Variant EditorSceneFormatImporterGLTF::get_option_visibility(const String &p_path, const String &p_scene_import_type, const String &p_option, const HashMap<StringName, Variant> &p_options) { return true; } diff --git a/modules/gltf/editor/editor_scene_importer_gltf.h b/modules/gltf/editor/editor_scene_importer_gltf.h index d0a9aaf05a..e17b6f3f2e 100644 --- a/modules/gltf/editor/editor_scene_importer_gltf.h +++ b/modules/gltf/editor/editor_scene_importer_gltf.h @@ -50,7 +50,7 @@ public: virtual void get_import_options(const String &p_path, List<ResourceImporter::ImportOption> *r_options) override; virtual void handle_compatibility_options(HashMap<StringName, Variant> &p_import_params) const override; - virtual Variant get_option_visibility(const String &p_path, bool p_for_animation, + virtual Variant get_option_visibility(const String &p_path, const String &p_scene_import_type, const String &p_option, const HashMap<StringName, Variant> &p_options) override; }; diff --git a/modules/gltf/extensions/gltf_document_extension_convert_importer_mesh.cpp b/modules/gltf/extensions/gltf_document_extension_convert_importer_mesh.cpp index 676f764c11..64117349e0 100644 --- a/modules/gltf/extensions/gltf_document_extension_convert_importer_mesh.cpp +++ b/modules/gltf/extensions/gltf_document_extension_convert_importer_mesh.cpp @@ -52,24 +52,24 @@ Error GLTFDocumentExtensionConvertImporterMesh::import_post(Ref<GLTFState> p_sta while (!queue.is_empty()) { List<Node *>::Element *E = queue.front(); Node *node = E->get(); - ImporterMeshInstance3D *mesh_3d = cast_to<ImporterMeshInstance3D>(node); - if (mesh_3d) { - MeshInstance3D *mesh_instance_node_3d = memnew(MeshInstance3D); - Ref<ImporterMesh> mesh = mesh_3d->get_mesh(); + ImporterMeshInstance3D *importer_mesh_3d = Object::cast_to<ImporterMeshInstance3D>(node); + if (importer_mesh_3d) { + Ref<ImporterMesh> mesh = importer_mesh_3d->get_mesh(); if (mesh.is_valid()) { + MeshInstance3D *mesh_instance_node_3d = memnew(MeshInstance3D); Ref<ArrayMesh> array_mesh = mesh->get_mesh(); mesh_instance_node_3d->set_name(node->get_name()); - mesh_instance_node_3d->set_transform(mesh_3d->get_transform()); + mesh_instance_node_3d->set_transform(importer_mesh_3d->get_transform()); mesh_instance_node_3d->set_mesh(array_mesh); - mesh_instance_node_3d->set_skin(mesh_3d->get_skin()); - mesh_instance_node_3d->set_skeleton_path(mesh_3d->get_skeleton_path()); + mesh_instance_node_3d->set_skin(importer_mesh_3d->get_skin()); + mesh_instance_node_3d->set_skeleton_path(importer_mesh_3d->get_skeleton_path()); node->replace_by(mesh_instance_node_3d); - _copy_meta(mesh_3d, mesh_instance_node_3d); + _copy_meta(importer_mesh_3d, mesh_instance_node_3d); _copy_meta(mesh.ptr(), array_mesh.ptr()); delete_queue.push_back(node); node = mesh_instance_node_3d; } else { - memdelete(mesh_instance_node_3d); + WARN_PRINT("glTF: ImporterMeshInstance3D does not have a valid mesh. This should not happen. Continuing anyway."); } } int child_count = node->get_child_count(); diff --git a/modules/gltf/gltf_document.cpp b/modules/gltf/gltf_document.cpp index cd25b93e6c..69973a34dd 100644 --- a/modules/gltf/gltf_document.cpp +++ b/modules/gltf/gltf_document.cpp @@ -69,6 +69,24 @@ #include <stdlib.h> #include <cstdint> +static void _attach_extras_to_meta(const Dictionary &p_extras, Ref<Resource> p_node) { + if (!p_extras.is_empty()) { + p_node->set_meta("extras", p_extras); + } +} + +static void _attach_meta_to_extras(Ref<Resource> p_node, Dictionary &p_json) { + if (p_node->has_meta("extras")) { + Dictionary node_extras = p_node->get_meta("extras"); + if (p_json.has("extras")) { + Dictionary extras = p_json["extras"]; + extras.merge(node_extras); + } else { + p_json["extras"] = node_extras; + } + } +} + static Ref<ImporterMesh> _mesh_to_importer_mesh(Ref<Mesh> p_mesh) { Ref<ImporterMesh> importer_mesh; importer_mesh.instantiate(); @@ -101,6 +119,7 @@ static Ref<ImporterMesh> _mesh_to_importer_mesh(Ref<Mesh> p_mesh) { array, p_mesh->surface_get_blend_shape_arrays(surface_i), p_mesh->surface_get_lods(surface_i), mat, mat_name, p_mesh->surface_get_format(surface_i)); } + importer_mesh->merge_meta_from(*p_mesh); return importer_mesh; } @@ -458,7 +477,7 @@ Error GLTFDocument::_serialize_nodes(Ref<GLTFState> p_state) { if (extensions.is_empty()) { node.erase("extensions"); } - + _attach_meta_to_extras(gltf_node, node); nodes.push_back(node); } if (!nodes.is_empty()) { @@ -624,6 +643,10 @@ Error GLTFDocument::_parse_nodes(Ref<GLTFState> p_state) { } } + if (n.has("extras")) { + _attach_extras_to_meta(n["extras"], node); + } + if (n.has("children")) { const Array &children = n["children"]; for (int j = 0; j < children.size(); j++) { @@ -2727,6 +2750,8 @@ Error GLTFDocument::_serialize_meshes(Ref<GLTFState> p_state) { Dictionary e; e["targetNames"] = target_names; + gltf_mesh["extras"] = e; + _attach_meta_to_extras(import_mesh, gltf_mesh); weights.resize(target_names.size()); for (int name_i = 0; name_i < target_names.size(); name_i++) { @@ -2742,8 +2767,6 @@ Error GLTFDocument::_serialize_meshes(Ref<GLTFState> p_state) { ERR_FAIL_COND_V(target_names.size() != weights.size(), FAILED); - gltf_mesh["extras"] = e; - gltf_mesh["primitives"] = primitives; meshes.push_back(gltf_mesh); @@ -2776,6 +2799,7 @@ Error GLTFDocument::_parse_meshes(Ref<GLTFState> p_state) { Array primitives = d["primitives"]; const Dictionary &extras = d.has("extras") ? (Dictionary)d["extras"] : Dictionary(); + _attach_extras_to_meta(extras, mesh); Ref<ImporterMesh> import_mesh; import_mesh.instantiate(); String mesh_name = "mesh"; @@ -4170,6 +4194,7 @@ Error GLTFDocument::_serialize_materials(Ref<GLTFState> p_state) { } d["extensions"] = extensions; + _attach_meta_to_extras(material, d); materials.push_back(d); } if (!materials.size()) { @@ -4372,6 +4397,10 @@ Error GLTFDocument::_parse_materials(Ref<GLTFState> p_state) { } } } + + if (material_dict.has("extras")) { + _attach_extras_to_meta(material_dict["extras"], material); + } p_state->materials.push_back(material); } @@ -5161,6 +5190,7 @@ ImporterMeshInstance3D *GLTFDocument::_generate_mesh_instance(Ref<GLTFState> p_s return mi; } mi->set_mesh(import_mesh); + import_mesh->merge_meta_from(*mesh); return mi; } @@ -5285,6 +5315,7 @@ void GLTFDocument::_convert_scene_node(Ref<GLTFState> p_state, Node *p_current, gltf_root = current_node_i; p_state->root_nodes.push_back(gltf_root); } + gltf_node->merge_meta_from(p_current); _create_gltf_node(p_state, p_current, current_node_i, p_gltf_parent, gltf_root, gltf_node); for (int node_i = 0; node_i < p_current->get_child_count(); node_i++) { _convert_scene_node(p_state, p_current->get_child(node_i), current_node_i, gltf_root); @@ -5676,6 +5707,8 @@ void GLTFDocument::_generate_scene_node(Ref<GLTFState> p_state, const GLTFNodeIn current_node->set_transform(gltf_node->transform); } + current_node->merge_meta_from(*gltf_node); + p_state->scene_nodes.insert(p_node_index, current_node); for (int i = 0; i < gltf_node->children.size(); ++i) { _generate_scene_node(p_state, gltf_node->children[i], current_node, p_scene_root); @@ -7060,6 +7093,8 @@ void GLTFDocument::_bind_methods() { &GLTFDocument::register_gltf_document_extension, DEFVAL(false)); ClassDB::bind_static_method("GLTFDocument", D_METHOD("unregister_gltf_document_extension", "extension"), &GLTFDocument::unregister_gltf_document_extension); + ClassDB::bind_static_method("GLTFDocument", D_METHOD("get_supported_gltf_extensions"), + &GLTFDocument::get_supported_gltf_extensions); } void GLTFDocument::_build_parent_hierachy(Ref<GLTFState> p_state) { @@ -7100,6 +7135,36 @@ Vector<Ref<GLTFDocumentExtension>> GLTFDocument::get_all_gltf_document_extension return all_document_extensions; } +Vector<String> GLTFDocument::get_supported_gltf_extensions() { + HashSet<String> set = get_supported_gltf_extensions_hashset(); + Vector<String> vec; + for (const String &s : set) { + vec.append(s); + } + vec.sort(); + return vec; +} + +HashSet<String> GLTFDocument::get_supported_gltf_extensions_hashset() { + HashSet<String> supported_extensions; + // If the extension is supported directly in GLTFDocument, list it here. + // Other built-in extensions are supported by GLTFDocumentExtension classes. + supported_extensions.insert("GODOT_single_root"); + supported_extensions.insert("KHR_lights_punctual"); + supported_extensions.insert("KHR_materials_emissive_strength"); + supported_extensions.insert("KHR_materials_pbrSpecularGlossiness"); + supported_extensions.insert("KHR_materials_unlit"); + supported_extensions.insert("KHR_texture_transform"); + for (Ref<GLTFDocumentExtension> ext : all_document_extensions) { + ERR_CONTINUE(ext.is_null()); + Vector<String> ext_supported_extensions = ext->get_supported_extensions(); + for (int i = 0; i < ext_supported_extensions.size(); ++i) { + supported_extensions.insert(ext_supported_extensions[i]); + } + } + return supported_extensions; +} + PackedByteArray GLTFDocument::_serialize_glb_buffer(Ref<GLTFState> p_state, Error *r_err) { Error err = _encode_buffer_glb(p_state, ""); if (r_err) { @@ -7452,19 +7517,7 @@ Error GLTFDocument::_parse_gltf_extensions(Ref<GLTFState> p_state) { Vector<String> ext_array = p_state->json["extensionsRequired"]; p_state->extensions_required = ext_array; } - HashSet<String> supported_extensions; - supported_extensions.insert("KHR_lights_punctual"); - supported_extensions.insert("KHR_materials_pbrSpecularGlossiness"); - supported_extensions.insert("KHR_texture_transform"); - supported_extensions.insert("KHR_materials_unlit"); - supported_extensions.insert("KHR_materials_emissive_strength"); - for (Ref<GLTFDocumentExtension> ext : document_extensions) { - ERR_CONTINUE(ext.is_null()); - Vector<String> ext_supported_extensions = ext->get_supported_extensions(); - for (int i = 0; i < ext_supported_extensions.size(); ++i) { - supported_extensions.insert(ext_supported_extensions[i]); - } - } + HashSet<String> supported_extensions = get_supported_gltf_extensions_hashset(); Error ret = OK; for (int i = 0; i < p_state->extensions_required.size(); i++) { if (!supported_extensions.has(p_state->extensions_required[i])) { diff --git a/modules/gltf/gltf_document.h b/modules/gltf/gltf_document.h index d37544750d..b3e6dcf54a 100644 --- a/modules/gltf/gltf_document.h +++ b/modules/gltf/gltf_document.h @@ -92,6 +92,8 @@ public: static void unregister_gltf_document_extension(Ref<GLTFDocumentExtension> p_extension); static void unregister_all_gltf_document_extensions(); static Vector<Ref<GLTFDocumentExtension>> get_all_gltf_document_extensions(); + static Vector<String> get_supported_gltf_extensions(); + static HashSet<String> get_supported_gltf_extensions_hashset(); void set_naming_version(int p_version); int get_naming_version() const; diff --git a/modules/gltf/tests/test_gltf_extras.h b/modules/gltf/tests/test_gltf_extras.h new file mode 100644 index 0000000000..96aadf3023 --- /dev/null +++ b/modules/gltf/tests/test_gltf_extras.h @@ -0,0 +1,165 @@ +/**************************************************************************/ +/* test_gltf_extras.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. */ +/**************************************************************************/ + +#ifndef TEST_GLTF_EXTRAS_H +#define TEST_GLTF_EXTRAS_H + +#include "tests/test_macros.h" + +#ifdef TOOLS_ENABLED + +#include "core/os/os.h" +#include "editor/import/3d/resource_importer_scene.h" +#include "modules/gltf/editor/editor_scene_importer_gltf.h" +#include "modules/gltf/gltf_document.h" +#include "modules/gltf/gltf_state.h" +#include "scene/3d/mesh_instance_3d.h" +#include "scene/main/window.h" +#include "scene/resources/3d/primitive_meshes.h" +#include "scene/resources/material.h" +#include "scene/resources/packed_scene.h" + +namespace TestGltfExtras { + +static Node *_gltf_export_then_import(Node *p_root, String &p_tempfilebase) { + Ref<GLTFDocument> doc; + doc.instantiate(); + Ref<GLTFState> state; + state.instantiate(); + Error err = doc->append_from_scene(p_root, state, EditorSceneFormatImporter::IMPORT_USE_NAMED_SKIN_BINDS); + CHECK_MESSAGE(err == OK, "GLTF state generation failed."); + err = doc->write_to_filesystem(state, p_tempfilebase + ".gltf"); + CHECK_MESSAGE(err == OK, "Writing GLTF to cache dir failed."); + + // Setting up importers. + Ref<ResourceImporterScene> import_scene = memnew(ResourceImporterScene("PackedScene", true)); + ResourceFormatImporter::get_singleton()->add_importer(import_scene); + Ref<EditorSceneFormatImporterGLTF> import_gltf; + import_gltf.instantiate(); + ResourceImporterScene::add_scene_importer(import_gltf); + + // GTLF importer behaves differently outside of editor, it's too late to modify Engine::get_editor_hint + // as the registration of runtime extensions already happened, so remove them. See modules/gltf/register_types.cpp + GLTFDocument::unregister_all_gltf_document_extensions(); + + HashMap<StringName, Variant> options(20); + options["nodes/root_type"] = ""; + options["nodes/root_name"] = ""; + options["nodes/apply_root_scale"] = true; + options["nodes/root_scale"] = 1.0; + options["meshes/ensure_tangents"] = true; + options["meshes/generate_lods"] = false; + options["meshes/create_shadow_meshes"] = true; + options["meshes/light_baking"] = 1; + options["meshes/lightmap_texel_size"] = 0.2; + options["meshes/force_disable_compression"] = false; + options["skins/use_named_skins"] = true; + options["animation/import"] = true; + options["animation/fps"] = 30; + options["animation/trimming"] = false; + options["animation/remove_immutable_tracks"] = true; + options["import_script/path"] = ""; + options["_subresources"] = Dictionary(); + options["gltf/naming_version"] = 1; + + // Process gltf file, note that this generates `.scn` resource from the 2nd argument. + err = import_scene->import(p_tempfilebase + ".gltf", p_tempfilebase, options, nullptr, nullptr, nullptr); + CHECK_MESSAGE(err == OK, "GLTF import failed."); + ResourceImporterScene::remove_scene_importer(import_gltf); + + Ref<PackedScene> packed_scene = ResourceLoader::load(p_tempfilebase + ".scn", "", ResourceFormatLoader::CACHE_MODE_REPLACE, &err); + CHECK_MESSAGE(err == OK, "Loading scene failed."); + Node *p_scene = packed_scene->instantiate(); + return p_scene; +} + +TEST_CASE("[SceneTree][Node] GLTF test mesh and material meta export and import") { + // Setup scene. + Ref<StandardMaterial3D> original_material = memnew(StandardMaterial3D); + original_material->set_albedo(Color(1.0, .0, .0)); + original_material->set_name("material"); + Dictionary material_dict; + material_dict["node_type"] = "material"; + original_material->set_meta("extras", material_dict); + + Ref<PlaneMesh> original_meshdata = memnew(PlaneMesh); + original_meshdata->set_name("planemesh"); + Dictionary meshdata_dict; + meshdata_dict["node_type"] = "planemesh"; + original_meshdata->set_meta("extras", meshdata_dict); + original_meshdata->surface_set_material(0, original_material); + + MeshInstance3D *original_mesh_instance = memnew(MeshInstance3D); + original_mesh_instance->set_mesh(original_meshdata); + original_mesh_instance->set_name("mesh_instance_3d"); + Dictionary mesh_instance_dict; + mesh_instance_dict["node_type"] = "mesh_instance_3d"; + original_mesh_instance->set_meta("extras", mesh_instance_dict); + + Node3D *original = memnew(Node3D); + SceneTree::get_singleton()->get_root()->add_child(original); + original->add_child(original_mesh_instance); + original->set_name("node3d"); + Dictionary node_dict; + node_dict["node_type"] = "node3d"; + original->set_meta("extras", node_dict); + original->set_meta("meta_not_nested_under_extras", "should not propagate"); + + // Convert to GLFT and back. + String tempfile = OS::get_singleton()->get_cache_path().path_join("gltf_extras"); + Node *loaded = _gltf_export_then_import(original, tempfile); + + // Compare the results. + CHECK(loaded->get_name() == "node3d"); + CHECK(Dictionary(loaded->get_meta("extras")).size() == 1); + CHECK(Dictionary(loaded->get_meta("extras"))["node_type"] == "node3d"); + CHECK_FALSE(loaded->has_meta("meta_not_nested_under_extras")); + CHECK_FALSE(Dictionary(loaded->get_meta("extras")).has("meta_not_nested_under_extras")); + + MeshInstance3D *mesh_instance_3d = Object::cast_to<MeshInstance3D>(loaded->find_child("mesh_instance_3d", false, true)); + CHECK(mesh_instance_3d->get_name() == "mesh_instance_3d"); + CHECK(Dictionary(mesh_instance_3d->get_meta("extras"))["node_type"] == "mesh_instance_3d"); + + Ref<Mesh> mesh = mesh_instance_3d->get_mesh(); + CHECK(Dictionary(mesh->get_meta("extras"))["node_type"] == "planemesh"); + + Ref<Material> material = mesh->surface_get_material(0); + CHECK(material->get_name() == "material"); + CHECK(Dictionary(material->get_meta("extras"))["node_type"] == "material"); + + memdelete(original_mesh_instance); + memdelete(original); + memdelete(loaded); +} +} // namespace TestGltfExtras + +#endif // TOOLS_ENABLED + +#endif // TEST_GLTF_EXTRAS_H diff --git a/modules/lightmapper_rd/lightmapper_rd.cpp b/modules/lightmapper_rd/lightmapper_rd.cpp index ddc51bcf6b..47a5b23895 100644 --- a/modules/lightmapper_rd/lightmapper_rd.cpp +++ b/modules/lightmapper_rd/lightmapper_rd.cpp @@ -1716,7 +1716,7 @@ LightmapperRD::BakeError LightmapperRD::bake(BakeQuality p_quality, bool p_use_d light_probe_buffer = rd->storage_buffer_create(sizeof(float) * 4 * 9 * probe_positions.size()); if (p_step_function) { - p_step_function(0.7, RTR("Baking lightprobes"), p_bake_userdata, true); + p_step_function(0.7, RTR("Baking light probes"), p_bake_userdata, true); } Vector<RD::Uniform> uniforms; diff --git a/modules/mbedtls/crypto_mbedtls.cpp b/modules/mbedtls/crypto_mbedtls.cpp index e910627b32..0d97b5fc1a 100644 --- a/modules/mbedtls/crypto_mbedtls.cpp +++ b/modules/mbedtls/crypto_mbedtls.cpp @@ -49,8 +49,8 @@ #define PEM_END_CRT "-----END CERTIFICATE-----\n" #define PEM_MIN_SIZE 54 -CryptoKey *CryptoKeyMbedTLS::create() { - return memnew(CryptoKeyMbedTLS); +CryptoKey *CryptoKeyMbedTLS::create(bool p_notify_postinitialize) { + return static_cast<CryptoKey *>(ClassDB::creator<CryptoKeyMbedTLS>(p_notify_postinitialize)); } Error CryptoKeyMbedTLS::load(const String &p_path, bool p_public_only) { @@ -153,8 +153,8 @@ int CryptoKeyMbedTLS::_parse_key(const uint8_t *p_buf, int p_size) { #endif } -X509Certificate *X509CertificateMbedTLS::create() { - return memnew(X509CertificateMbedTLS); +X509Certificate *X509CertificateMbedTLS::create(bool p_notify_postinitialize) { + return static_cast<X509Certificate *>(ClassDB::creator<X509CertificateMbedTLS>(p_notify_postinitialize)); } Error X509CertificateMbedTLS::load(const String &p_path) { @@ -250,8 +250,8 @@ bool HMACContextMbedTLS::is_md_type_allowed(mbedtls_md_type_t p_md_type) { } } -HMACContext *HMACContextMbedTLS::create() { - return memnew(HMACContextMbedTLS); +HMACContext *HMACContextMbedTLS::create(bool p_notify_postinitialize) { + return static_cast<HMACContext *>(ClassDB::creator<HMACContextMbedTLS>(p_notify_postinitialize)); } Error HMACContextMbedTLS::start(HashingContext::HashType p_hash_type, const PackedByteArray &p_key) { @@ -309,8 +309,8 @@ HMACContextMbedTLS::~HMACContextMbedTLS() { } } -Crypto *CryptoMbedTLS::create() { - return memnew(CryptoMbedTLS); +Crypto *CryptoMbedTLS::create(bool p_notify_postinitialize) { + return static_cast<Crypto *>(ClassDB::creator<CryptoMbedTLS>(p_notify_postinitialize)); } void CryptoMbedTLS::initialize_crypto() { diff --git a/modules/mbedtls/crypto_mbedtls.h b/modules/mbedtls/crypto_mbedtls.h index 52918cedf0..5e1da550d7 100644 --- a/modules/mbedtls/crypto_mbedtls.h +++ b/modules/mbedtls/crypto_mbedtls.h @@ -49,7 +49,7 @@ private: int _parse_key(const uint8_t *p_buf, int p_size); public: - static CryptoKey *create(); + static CryptoKey *create(bool p_notify_postinitialize = true); static void make_default() { CryptoKey::_create = create; } static void finalize() { CryptoKey::_create = nullptr; } @@ -80,7 +80,7 @@ private: int locks; public: - static X509Certificate *create(); + static X509Certificate *create(bool p_notify_postinitialize = true); static void make_default() { X509Certificate::_create = create; } static void finalize() { X509Certificate::_create = nullptr; } @@ -112,7 +112,7 @@ private: void *ctx = nullptr; public: - static HMACContext *create(); + static HMACContext *create(bool p_notify_postinitialize = true); static void make_default() { HMACContext::_create = create; } static void finalize() { HMACContext::_create = nullptr; } @@ -133,7 +133,7 @@ private: static X509CertificateMbedTLS *default_certs; public: - static Crypto *create(); + static Crypto *create(bool p_notify_postinitialize = true); static void initialize_crypto(); static void finalize_crypto(); static X509CertificateMbedTLS *get_default_certificates(); diff --git a/modules/mbedtls/dtls_server_mbedtls.cpp b/modules/mbedtls/dtls_server_mbedtls.cpp index e466fe15d6..b64bdcb192 100644 --- a/modules/mbedtls/dtls_server_mbedtls.cpp +++ b/modules/mbedtls/dtls_server_mbedtls.cpp @@ -54,8 +54,8 @@ Ref<PacketPeerDTLS> DTLSServerMbedTLS::take_connection(Ref<PacketPeerUDP> p_udp_ return out; } -DTLSServer *DTLSServerMbedTLS::_create_func() { - return memnew(DTLSServerMbedTLS); +DTLSServer *DTLSServerMbedTLS::_create_func(bool p_notify_postinitialize) { + return static_cast<DTLSServer *>(ClassDB::creator<DTLSServerMbedTLS>(p_notify_postinitialize)); } void DTLSServerMbedTLS::initialize() { diff --git a/modules/mbedtls/dtls_server_mbedtls.h b/modules/mbedtls/dtls_server_mbedtls.h index 59befecf43..18661bf505 100644 --- a/modules/mbedtls/dtls_server_mbedtls.h +++ b/modules/mbedtls/dtls_server_mbedtls.h @@ -37,7 +37,7 @@ class DTLSServerMbedTLS : public DTLSServer { private: - static DTLSServer *_create_func(); + static DTLSServer *_create_func(bool p_notify_postinitialize); Ref<TLSOptions> tls_options; Ref<CookieContextMbedTLS> cookies; diff --git a/modules/mbedtls/packet_peer_mbed_dtls.cpp b/modules/mbedtls/packet_peer_mbed_dtls.cpp index c7373481ca..62d27405d8 100644 --- a/modules/mbedtls/packet_peer_mbed_dtls.cpp +++ b/modules/mbedtls/packet_peer_mbed_dtls.cpp @@ -270,8 +270,8 @@ PacketPeerMbedDTLS::Status PacketPeerMbedDTLS::get_status() const { return status; } -PacketPeerDTLS *PacketPeerMbedDTLS::_create_func() { - return memnew(PacketPeerMbedDTLS); +PacketPeerDTLS *PacketPeerMbedDTLS::_create_func(bool p_notify_postinitialize) { + return static_cast<PacketPeerDTLS *>(ClassDB::creator<PacketPeerMbedDTLS>(p_notify_postinitialize)); } void PacketPeerMbedDTLS::initialize_dtls() { diff --git a/modules/mbedtls/packet_peer_mbed_dtls.h b/modules/mbedtls/packet_peer_mbed_dtls.h index 2cff7a3589..881a5fdd0e 100644 --- a/modules/mbedtls/packet_peer_mbed_dtls.h +++ b/modules/mbedtls/packet_peer_mbed_dtls.h @@ -50,7 +50,7 @@ private: Ref<PacketPeerUDP> base; - static PacketPeerDTLS *_create_func(); + static PacketPeerDTLS *_create_func(bool p_notify_postinitialize); static int bio_recv(void *ctx, unsigned char *buf, size_t len); static int bio_send(void *ctx, const unsigned char *buf, size_t len); diff --git a/modules/mbedtls/stream_peer_mbedtls.cpp b/modules/mbedtls/stream_peer_mbedtls.cpp index a359b42041..b4200410fb 100644 --- a/modules/mbedtls/stream_peer_mbedtls.cpp +++ b/modules/mbedtls/stream_peer_mbedtls.cpp @@ -295,8 +295,8 @@ Ref<StreamPeer> StreamPeerMbedTLS::get_stream() const { return base; } -StreamPeerTLS *StreamPeerMbedTLS::_create_func() { - return memnew(StreamPeerMbedTLS); +StreamPeerTLS *StreamPeerMbedTLS::_create_func(bool p_notify_postinitialize) { + return static_cast<StreamPeerTLS *>(ClassDB::creator<StreamPeerMbedTLS>(p_notify_postinitialize)); } void StreamPeerMbedTLS::initialize_tls() { diff --git a/modules/mbedtls/stream_peer_mbedtls.h b/modules/mbedtls/stream_peer_mbedtls.h index a8080f0960..b4f80b614c 100644 --- a/modules/mbedtls/stream_peer_mbedtls.h +++ b/modules/mbedtls/stream_peer_mbedtls.h @@ -42,7 +42,7 @@ private: Ref<StreamPeer> base; - static StreamPeerTLS *_create_func(); + static StreamPeerTLS *_create_func(bool p_notify_postinitialize); static int bio_recv(void *ctx, unsigned char *buf, size_t len); static int bio_send(void *ctx, const unsigned char *buf, size_t len); diff --git a/modules/mbedtls/tls_context_mbedtls.cpp b/modules/mbedtls/tls_context_mbedtls.cpp index eaea7b9293..f5c196596e 100644 --- a/modules/mbedtls/tls_context_mbedtls.cpp +++ b/modules/mbedtls/tls_context_mbedtls.cpp @@ -153,7 +153,7 @@ Error TLSContextMbedTLS::init_client(int p_transport, const String &p_hostname, int authmode = MBEDTLS_SSL_VERIFY_REQUIRED; bool unsafe = p_options->is_unsafe_client(); - if (unsafe && p_options->get_trusted_ca_chain().is_valid()) { + if (unsafe && p_options->get_trusted_ca_chain().is_null()) { authmode = MBEDTLS_SSL_VERIFY_NONE; } diff --git a/modules/minimp3/audio_stream_mp3.cpp b/modules/minimp3/audio_stream_mp3.cpp index 5720f844bb..394213963a 100644 --- a/modules/minimp3/audio_stream_mp3.cpp +++ b/modules/minimp3/audio_stream_mp3.cpp @@ -57,7 +57,7 @@ int AudioStreamPlaybackMP3::_mix_internal(AudioFrame *p_buffer, int p_frames) { mp3dec_frame_info_t frame_info; mp3d_sample_t *buf_frame = nullptr; - int samples_mixed = mp3dec_ex_read_frame(mp3d, &buf_frame, &frame_info, mp3_stream->channels); + int samples_mixed = mp3dec_ex_read_frame(&mp3d, &buf_frame, &frame_info, mp3_stream->channels); if (samples_mixed) { p_buffer[p_frames - todo] = AudioFrame(buf_frame[0], buf_frame[samples_mixed - 1]); @@ -70,7 +70,7 @@ int AudioStreamPlaybackMP3::_mix_internal(AudioFrame *p_buffer, int p_frames) { if (beat_loop && (int)frames_mixed >= beat_length_frames) { for (int i = 0; i < FADE_SIZE; i++) { - samples_mixed = mp3dec_ex_read_frame(mp3d, &buf_frame, &frame_info, mp3_stream->channels); + samples_mixed = mp3dec_ex_read_frame(&mp3d, &buf_frame, &frame_info, mp3_stream->channels); loop_fade[i] = AudioFrame(buf_frame[0], buf_frame[samples_mixed - 1]); if (!samples_mixed) { break; @@ -138,7 +138,7 @@ void AudioStreamPlaybackMP3::seek(double p_time) { } frames_mixed = uint32_t(mp3_stream->sample_rate * p_time); - mp3dec_ex_seek(mp3d, (uint64_t)frames_mixed * mp3_stream->channels); + mp3dec_ex_seek(&mp3d, (uint64_t)frames_mixed * mp3_stream->channels); } void AudioStreamPlaybackMP3::tag_used_streams() { @@ -181,10 +181,7 @@ Variant AudioStreamPlaybackMP3::get_parameter(const StringName &p_name) const { } AudioStreamPlaybackMP3::~AudioStreamPlaybackMP3() { - if (mp3d) { - mp3dec_ex_close(mp3d); - memfree(mp3d); - } + mp3dec_ex_close(&mp3d); } Ref<AudioStreamPlayback> AudioStreamMP3::instantiate_playback() { @@ -197,9 +194,8 @@ Ref<AudioStreamPlayback> AudioStreamMP3::instantiate_playback() { mp3s.instantiate(); mp3s->mp3_stream = Ref<AudioStreamMP3>(this); - mp3s->mp3d = (mp3dec_ex_t *)memalloc(sizeof(mp3dec_ex_t)); - int errorcode = mp3dec_ex_open_buf(mp3s->mp3d, data.ptr(), data_len, MP3D_SEEK_TO_SAMPLE); + int errorcode = mp3dec_ex_open_buf(&mp3s->mp3d, data.ptr(), data_len, MP3D_SEEK_TO_SAMPLE); mp3s->frames_mixed = 0; mp3s->active = false; @@ -224,15 +220,19 @@ void AudioStreamMP3::set_data(const Vector<uint8_t> &p_data) { int src_data_len = p_data.size(); const uint8_t *src_datar = p_data.ptr(); - mp3dec_ex_t mp3d; - int err = mp3dec_ex_open_buf(&mp3d, src_datar, src_data_len, MP3D_SEEK_TO_SAMPLE); - ERR_FAIL_COND_MSG(err || mp3d.info.hz == 0, "Failed to decode mp3 file. Make sure it is a valid mp3 audio file."); + mp3dec_ex_t *mp3d = memnew(mp3dec_ex_t); + int err = mp3dec_ex_open_buf(mp3d, src_datar, src_data_len, MP3D_SEEK_TO_SAMPLE); + if (err || mp3d->info.hz == 0) { + memdelete(mp3d); + ERR_FAIL_MSG("Failed to decode mp3 file. Make sure it is a valid mp3 audio file."); + } - channels = mp3d.info.channels; - sample_rate = mp3d.info.hz; - length = float(mp3d.samples) / (sample_rate * float(channels)); + channels = mp3d->info.channels; + sample_rate = mp3d->info.hz; + length = float(mp3d->samples) / (sample_rate * float(channels)); - mp3dec_ex_close(&mp3d); + mp3dec_ex_close(mp3d); + memdelete(mp3d); clear_data(); diff --git a/modules/minimp3/audio_stream_mp3.h b/modules/minimp3/audio_stream_mp3.h index 81e8f8633c..39d389b8cd 100644 --- a/modules/minimp3/audio_stream_mp3.h +++ b/modules/minimp3/audio_stream_mp3.h @@ -49,7 +49,7 @@ class AudioStreamPlaybackMP3 : public AudioStreamPlaybackResampled { bool looping_override = false; bool looping = false; - mp3dec_ex_t *mp3d = nullptr; + mp3dec_ex_t mp3d = {}; uint32_t frames_mixed = 0; bool active = false; int loops = 0; diff --git a/modules/mono/editor/GodotTools/GodotTools.BuildLogger/GodotBuildLogger.cs b/modules/mono/editor/GodotTools/GodotTools.BuildLogger/GodotBuildLogger.cs index b699765b8e..032d067ae4 100644 --- a/modules/mono/editor/GodotTools/GodotTools.BuildLogger/GodotBuildLogger.cs +++ b/modules/mono/editor/GodotTools/GodotTools.BuildLogger/GodotBuildLogger.cs @@ -86,7 +86,7 @@ namespace GodotTools.BuildLogger WriteLine(line); - string errorLine = $@"error,{e.File.CsvEscape()},{e.LineNumber},{e.ColumnNumber}," + + string errorLine = $@"error,{e.File?.CsvEscape() ?? string.Empty},{e.LineNumber},{e.ColumnNumber}," + $"{e.Code?.CsvEscape() ?? string.Empty},{e.Message.CsvEscape()}," + $"{e.ProjectFile?.CsvEscape() ?? string.Empty}"; _issuesStreamWriter.WriteLine(errorLine); @@ -101,7 +101,7 @@ namespace GodotTools.BuildLogger WriteLine(line); - string warningLine = $@"warning,{e.File.CsvEscape()},{e.LineNumber},{e.ColumnNumber}," + + string warningLine = $@"warning,{e.File?.CsvEscape() ?? string.Empty},{e.LineNumber},{e.ColumnNumber}," + $"{e.Code?.CsvEscape() ?? string.Empty},{e.Message.CsvEscape()}," + $"{e.ProjectFile?.CsvEscape() ?? string.Empty}"; _issuesStreamWriter.WriteLine(warningLine); diff --git a/modules/mono/editor/GodotTools/GodotTools/Build/BuildManager.cs b/modules/mono/editor/GodotTools/GodotTools/Build/BuildManager.cs index ebb2677361..d7877fa5fc 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Build/BuildManager.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Build/BuildManager.cs @@ -265,11 +265,6 @@ namespace GodotTools.Build success = Publish(buildInfo); } - if (!success) - { - ShowBuildErrorDialog("Failed to publish .NET project"); - } - return success; } diff --git a/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs b/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs index ede0600ac1..a5f24fb67b 100644 --- a/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs +++ b/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs @@ -75,7 +75,19 @@ namespace GodotTools.Export }; } - private string? _maybeLastExportError; + private void AddExceptionMessage(EditorExportPlatform platform, Exception exception) + { + string? exceptionMessage = exception.Message; + if (string.IsNullOrEmpty(exceptionMessage)) + { + exceptionMessage = $"Exception thrown: {exception.GetType().Name}"; + } + + platform.AddMessage(EditorExportPlatform.ExportMessageType.Error, "Export .NET Project", exceptionMessage); + + // We also print exceptions as we receive them to stderr. + Console.Error.WriteLine(exception); + } // With this method we can override how a file is exported in the PCK public override void _ExportFile(string path, string type, string[] features) @@ -92,8 +104,8 @@ namespace GodotTools.Export if (!ProjectContainsDotNet()) { - _maybeLastExportError = $"This project contains C# files but no solution file was found at the following path: {GodotSharpDirs.ProjectSlnPath}\n" + - "A solution file is required for projects with C# files. Please ensure that the solution file exists in the specified location and try again."; + GetExportPlatform().AddMessage(EditorExportPlatform.ExportMessageType.Error, "Export .NET Project", $"This project contains C# files but no solution file was found at the following path: {GodotSharpDirs.ProjectSlnPath}\n" + + "A solution file is required for projects with C# files. Please ensure that the solution file exists in the specified location and try again."); throw new InvalidOperationException($"{path} is a C# file but no solution file exists."); } @@ -124,16 +136,7 @@ namespace GodotTools.Export } catch (Exception e) { - _maybeLastExportError = e.Message; - - // 'maybeLastExportError' cannot be null or empty if there was an error, so we - // must consider the possibility of exceptions being thrown without a message. - if (string.IsNullOrEmpty(_maybeLastExportError)) - _maybeLastExportError = $"Exception thrown: {e.GetType().Name}"; - - GD.PushError($"Failed to export project: {_maybeLastExportError}"); - Console.Error.WriteLine(e); - // TODO: Do something on error once _ExportBegin supports failing. + AddExceptionMessage(GetExportPlatform(), e); } } @@ -144,7 +147,9 @@ namespace GodotTools.Export if (!ProjectContainsDotNet()) return; - if (!DeterminePlatformFromFeatures(features, out string? platform)) + string osName = GetExportPlatform().GetOsName(); + + if (!TryDeterminePlatformFromOSName(osName, out string? platform)) throw new NotSupportedException("Target platform not supported."); if (!new[] { OS.Platforms.Windows, OS.Platforms.LinuxBSD, OS.Platforms.MacOS, OS.Platforms.Android, OS.Platforms.iOS } @@ -445,25 +450,22 @@ namespace GodotTools.Export Directory.Delete(folder, recursive: true); } _tempFolders.Clear(); - - // TODO: The following is just a workaround until the export plugins can be made to abort with errors - - // We check for empty as well, because it's set to empty after hot-reloading - if (!string.IsNullOrEmpty(_maybeLastExportError)) - { - string lastExportError = _maybeLastExportError; - _maybeLastExportError = null; - - GodotSharpEditor.Instance.ShowErrorDialog(lastExportError, "Failed to export C# project"); - } } - private static bool DeterminePlatformFromFeatures(IEnumerable<string> features, [NotNullWhen(true)] out string? platform) + /// <summary> + /// Tries to determine the platform from the export preset's platform OS name. + /// </summary> + /// <param name="osName">Name of the export operating system.</param> + /// <param name="platform">Platform name for the recognized supported platform.</param> + /// <returns> + /// <see langword="true"/> when the platform OS name is recognized as a supported platform, + /// <see langword="false"/> otherwise. + /// </returns> + private static bool TryDeterminePlatformFromOSName(string osName, [NotNullWhen(true)] out string? platform) { - foreach (var feature in features) + if (OS.PlatformFeatureMap.TryGetValue(osName, out platform)) { - if (OS.PlatformFeatureMap.TryGetValue(feature, out platform)) - return true; + return true; } platform = null; diff --git a/modules/mono/editor/bindings_generator.cpp b/modules/mono/editor/bindings_generator.cpp index 9a76a25639..b26f6d1bbf 100644 --- a/modules/mono/editor/bindings_generator.cpp +++ b/modules/mono/editor/bindings_generator.cpp @@ -2184,7 +2184,7 @@ Error BindingsGenerator::_generate_cs_type(const TypeInterface &itype, const Str // Add native constructor static field output << MEMBER_BEGIN << "[DebuggerBrowsable(DebuggerBrowsableState.Never)]\n" - << INDENT1 "private static readonly unsafe delegate* unmanaged<IntPtr> " + << INDENT1 "private static readonly unsafe delegate* unmanaged<godot_bool, IntPtr> " << CS_STATIC_FIELD_NATIVE_CTOR " = " ICALL_CLASSDB_GET_CONSTRUCTOR << "(" BINDINGS_NATIVE_NAME_FIELD ");\n"; } diff --git a/modules/mono/editor/hostfxr_resolver.cpp b/modules/mono/editor/hostfxr_resolver.cpp index 7fa482969e..9c37ac810a 100644 --- a/modules/mono/editor/hostfxr_resolver.cpp +++ b/modules/mono/editor/hostfxr_resolver.cpp @@ -216,6 +216,7 @@ bool get_default_installation_dir(String &r_dotnet_root) { #endif } +#ifndef WINDOWS_ENABLED bool get_install_location_from_file(const String &p_file_path, String &r_dotnet_root) { Error err = OK; Ref<FileAccess> f = FileAccess::open(p_file_path, FileAccess::READ, &err); @@ -233,6 +234,7 @@ bool get_install_location_from_file(const String &p_file_path, String &r_dotnet_ r_dotnet_root = line; return true; } +#endif bool get_dotnet_self_registered_dir(String &r_dotnet_root) { #if defined(WINDOWS_ENABLED) @@ -260,7 +262,7 @@ bool get_dotnet_self_registered_dir(String &r_dotnet_root) { return false; } - r_dotnet_root = String::utf16((const char16_t *)buffer.ptr()); + r_dotnet_root = String::utf16((const char16_t *)buffer.ptr()).replace("\\", "/"); RegCloseKey(hkey); return true; #else diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/GodotObject.base.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/GodotObject.base.cs index 0be9cdc953..c094eaed77 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/GodotObject.base.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/GodotObject.base.cs @@ -30,7 +30,7 @@ namespace Godot } internal unsafe void ConstructAndInitialize( - delegate* unmanaged<IntPtr> nativeCtor, + delegate* unmanaged<godot_bool, IntPtr> nativeCtor, StringName nativeName, Type cachedType, bool refCounted @@ -40,7 +40,8 @@ namespace Godot { Debug.Assert(nativeCtor != null); - NativePtr = nativeCtor(); + // Need postinitialization. + NativePtr = nativeCtor(godot_bool.True); InteropUtils.TieManagedToUnmanaged(this, NativePtr, nativeName, refCounted, GetType(), cachedType); @@ -260,7 +261,7 @@ namespace Godot return methodBind; } - internal static unsafe delegate* unmanaged<IntPtr> ClassDB_get_constructor(StringName type) + internal static unsafe delegate* unmanaged<godot_bool, IntPtr> ClassDB_get_constructor(StringName type) { // for some reason the '??' operator doesn't support 'delegate*' var typeSelf = (godot_string_name)type.NativeValue; diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/NativeFuncs.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/NativeFuncs.cs index cfd9ed7acc..6a643833f6 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/NativeFuncs.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/NativeFuncs.cs @@ -47,7 +47,7 @@ namespace Godot.NativeInterop public static partial IntPtr godotsharp_method_bind_get_method_with_compatibility( in godot_string_name p_classname, in godot_string_name p_methodname, ulong p_hash); - public static partial delegate* unmanaged<IntPtr> godotsharp_get_class_constructor( + public static partial delegate* unmanaged<godot_bool, IntPtr> godotsharp_get_class_constructor( in godot_string_name p_classname); public static partial IntPtr godotsharp_engine_get_singleton(in godot_string p_name); diff --git a/modules/mono/glue/runtime_interop.cpp b/modules/mono/glue/runtime_interop.cpp index 80e9fdf77f..73c10eba83 100644 --- a/modules/mono/glue/runtime_interop.cpp +++ b/modules/mono/glue/runtime_interop.cpp @@ -58,7 +58,7 @@ extern "C" { // For ArrayPrivate and DictionaryPrivate static_assert(sizeof(SafeRefCount) == sizeof(uint32_t)); -typedef Object *(*godotsharp_class_creation_func)(); +typedef Object *(*godotsharp_class_creation_func)(bool); bool godotsharp_dotnet_module_is_initialized() { return GDMono::get_singleton()->is_initialized(); diff --git a/modules/multiplayer/editor/editor_network_profiler.cpp b/modules/multiplayer/editor/editor_network_profiler.cpp index d5d4b465d8..212fd1ef6b 100644 --- a/modules/multiplayer/editor/editor_network_profiler.cpp +++ b/modules/multiplayer/editor/editor_network_profiler.cpp @@ -227,10 +227,10 @@ void EditorNetworkProfiler::add_sync_frame_data(const SyncInfo &p_frame) { sync_data[p_frame.synchronizer].outgoing_syncs += p_frame.outgoing_syncs; } SyncInfo &info = sync_data[p_frame.synchronizer]; - if (info.incoming_syncs) { + if (p_frame.incoming_syncs) { info.incoming_size = p_frame.incoming_size / p_frame.incoming_syncs; } - if (info.outgoing_syncs) { + if (p_frame.outgoing_syncs) { info.outgoing_size = p_frame.outgoing_size / p_frame.outgoing_syncs; } } diff --git a/modules/navigation/2d/nav_mesh_generator_2d.cpp b/modules/navigation/2d/nav_mesh_generator_2d.cpp index 33b92f6266..78983187c7 100644 --- a/modules/navigation/2d/nav_mesh_generator_2d.cpp +++ b/modules/navigation/2d/nav_mesh_generator_2d.cpp @@ -87,57 +87,55 @@ void NavMeshGenerator2D::sync() { return; } - baking_navmesh_mutex.lock(); - generator_task_mutex.lock(); + MutexLock baking_navmesh_lock(baking_navmesh_mutex); + { + MutexLock generator_task_lock(generator_task_mutex); - LocalVector<WorkerThreadPool::TaskID> finished_task_ids; + LocalVector<WorkerThreadPool::TaskID> finished_task_ids; - for (KeyValue<WorkerThreadPool::TaskID, NavMeshGeneratorTask2D *> &E : generator_tasks) { - if (WorkerThreadPool::get_singleton()->is_task_completed(E.key)) { - WorkerThreadPool::get_singleton()->wait_for_task_completion(E.key); - finished_task_ids.push_back(E.key); + for (KeyValue<WorkerThreadPool::TaskID, NavMeshGeneratorTask2D *> &E : generator_tasks) { + if (WorkerThreadPool::get_singleton()->is_task_completed(E.key)) { + WorkerThreadPool::get_singleton()->wait_for_task_completion(E.key); + finished_task_ids.push_back(E.key); - NavMeshGeneratorTask2D *generator_task = E.value; - DEV_ASSERT(generator_task->status == NavMeshGeneratorTask2D::TaskStatus::BAKING_FINISHED); + NavMeshGeneratorTask2D *generator_task = E.value; + DEV_ASSERT(generator_task->status == NavMeshGeneratorTask2D::TaskStatus::BAKING_FINISHED); - baking_navmeshes.erase(generator_task->navigation_mesh); - if (generator_task->callback.is_valid()) { - generator_emit_callback(generator_task->callback); + baking_navmeshes.erase(generator_task->navigation_mesh); + if (generator_task->callback.is_valid()) { + generator_emit_callback(generator_task->callback); + } + memdelete(generator_task); } - memdelete(generator_task); } - } - for (WorkerThreadPool::TaskID finished_task_id : finished_task_ids) { - generator_tasks.erase(finished_task_id); + for (WorkerThreadPool::TaskID finished_task_id : finished_task_ids) { + generator_tasks.erase(finished_task_id); + } } - - generator_task_mutex.unlock(); - baking_navmesh_mutex.unlock(); } void NavMeshGenerator2D::cleanup() { - baking_navmesh_mutex.lock(); - generator_task_mutex.lock(); + MutexLock baking_navmesh_lock(baking_navmesh_mutex); + { + MutexLock generator_task_lock(generator_task_mutex); - baking_navmeshes.clear(); + baking_navmeshes.clear(); - for (KeyValue<WorkerThreadPool::TaskID, NavMeshGeneratorTask2D *> &E : generator_tasks) { - WorkerThreadPool::get_singleton()->wait_for_task_completion(E.key); - NavMeshGeneratorTask2D *generator_task = E.value; - memdelete(generator_task); - } - generator_tasks.clear(); + for (KeyValue<WorkerThreadPool::TaskID, NavMeshGeneratorTask2D *> &E : generator_tasks) { + WorkerThreadPool::get_singleton()->wait_for_task_completion(E.key); + NavMeshGeneratorTask2D *generator_task = E.value; + memdelete(generator_task); + } + generator_tasks.clear(); - generator_rid_rwlock.write_lock(); - for (NavMeshGeometryParser2D *parser : generator_parsers) { - generator_parser_owner.free(parser->self); + generator_rid_rwlock.write_lock(); + for (NavMeshGeometryParser2D *parser : generator_parsers) { + generator_parser_owner.free(parser->self); + } + generator_parsers.clear(); + generator_rid_rwlock.write_unlock(); } - generator_parsers.clear(); - generator_rid_rwlock.write_unlock(); - - generator_task_mutex.unlock(); - baking_navmesh_mutex.unlock(); } void NavMeshGenerator2D::finish() { @@ -212,7 +210,7 @@ void NavMeshGenerator2D::bake_from_source_geometry_data_async(Ref<NavigationPoly baking_navmeshes.insert(p_navigation_mesh); baking_navmesh_mutex.unlock(); - generator_task_mutex.lock(); + MutexLock generator_task_lock(generator_task_mutex); NavMeshGeneratorTask2D *generator_task = memnew(NavMeshGeneratorTask2D); generator_task->navigation_mesh = p_navigation_mesh; generator_task->source_geometry_data = p_source_geometry_data; @@ -220,14 +218,11 @@ void NavMeshGenerator2D::bake_from_source_geometry_data_async(Ref<NavigationPoly generator_task->status = NavMeshGeneratorTask2D::TaskStatus::BAKING_STARTED; generator_task->thread_task_id = WorkerThreadPool::get_singleton()->add_native_task(&NavMeshGenerator2D::generator_thread_bake, generator_task, NavMeshGenerator2D::baking_use_high_priority_threads, "NavMeshGeneratorBake2D"); generator_tasks.insert(generator_task->thread_task_id, generator_task); - generator_task_mutex.unlock(); } bool NavMeshGenerator2D::is_baking(Ref<NavigationPolygon> p_navigation_polygon) { - baking_navmesh_mutex.lock(); - bool baking = baking_navmeshes.has(p_navigation_polygon); - baking_navmesh_mutex.unlock(); - return baking; + MutexLock baking_navmesh_lock(baking_navmesh_mutex); + return baking_navmeshes.has(p_navigation_polygon); } void NavMeshGenerator2D::generator_thread_bake(void *p_arg) { diff --git a/modules/navigation/3d/nav_mesh_generator_3d.cpp b/modules/navigation/3d/nav_mesh_generator_3d.cpp index d17724baa0..e92a9d304b 100644 --- a/modules/navigation/3d/nav_mesh_generator_3d.cpp +++ b/modules/navigation/3d/nav_mesh_generator_3d.cpp @@ -100,57 +100,55 @@ void NavMeshGenerator3D::sync() { return; } - baking_navmesh_mutex.lock(); - generator_task_mutex.lock(); + MutexLock baking_navmesh_lock(baking_navmesh_mutex); + { + MutexLock generator_task_lock(generator_task_mutex); - LocalVector<WorkerThreadPool::TaskID> finished_task_ids; + LocalVector<WorkerThreadPool::TaskID> finished_task_ids; - for (KeyValue<WorkerThreadPool::TaskID, NavMeshGeneratorTask3D *> &E : generator_tasks) { - if (WorkerThreadPool::get_singleton()->is_task_completed(E.key)) { - WorkerThreadPool::get_singleton()->wait_for_task_completion(E.key); - finished_task_ids.push_back(E.key); + for (KeyValue<WorkerThreadPool::TaskID, NavMeshGeneratorTask3D *> &E : generator_tasks) { + if (WorkerThreadPool::get_singleton()->is_task_completed(E.key)) { + WorkerThreadPool::get_singleton()->wait_for_task_completion(E.key); + finished_task_ids.push_back(E.key); - NavMeshGeneratorTask3D *generator_task = E.value; - DEV_ASSERT(generator_task->status == NavMeshGeneratorTask3D::TaskStatus::BAKING_FINISHED); + NavMeshGeneratorTask3D *generator_task = E.value; + DEV_ASSERT(generator_task->status == NavMeshGeneratorTask3D::TaskStatus::BAKING_FINISHED); - baking_navmeshes.erase(generator_task->navigation_mesh); - if (generator_task->callback.is_valid()) { - generator_emit_callback(generator_task->callback); + baking_navmeshes.erase(generator_task->navigation_mesh); + if (generator_task->callback.is_valid()) { + generator_emit_callback(generator_task->callback); + } + memdelete(generator_task); } - memdelete(generator_task); } - } - for (WorkerThreadPool::TaskID finished_task_id : finished_task_ids) { - generator_tasks.erase(finished_task_id); + for (WorkerThreadPool::TaskID finished_task_id : finished_task_ids) { + generator_tasks.erase(finished_task_id); + } } - - generator_task_mutex.unlock(); - baking_navmesh_mutex.unlock(); } void NavMeshGenerator3D::cleanup() { - baking_navmesh_mutex.lock(); - generator_task_mutex.lock(); + MutexLock baking_navmesh_lock(baking_navmesh_mutex); + { + MutexLock generator_task_lock(generator_task_mutex); - baking_navmeshes.clear(); + baking_navmeshes.clear(); - for (KeyValue<WorkerThreadPool::TaskID, NavMeshGeneratorTask3D *> &E : generator_tasks) { - WorkerThreadPool::get_singleton()->wait_for_task_completion(E.key); - NavMeshGeneratorTask3D *generator_task = E.value; - memdelete(generator_task); - } - generator_tasks.clear(); + for (KeyValue<WorkerThreadPool::TaskID, NavMeshGeneratorTask3D *> &E : generator_tasks) { + WorkerThreadPool::get_singleton()->wait_for_task_completion(E.key); + NavMeshGeneratorTask3D *generator_task = E.value; + memdelete(generator_task); + } + generator_tasks.clear(); - generator_rid_rwlock.write_lock(); - for (NavMeshGeometryParser3D *parser : generator_parsers) { - generator_parser_owner.free(parser->self); + generator_rid_rwlock.write_lock(); + for (NavMeshGeometryParser3D *parser : generator_parsers) { + generator_parser_owner.free(parser->self); + } + generator_parsers.clear(); + generator_rid_rwlock.write_unlock(); } - generator_parsers.clear(); - generator_rid_rwlock.write_unlock(); - - generator_task_mutex.unlock(); - baking_navmesh_mutex.unlock(); } void NavMeshGenerator3D::finish() { @@ -226,7 +224,7 @@ void NavMeshGenerator3D::bake_from_source_geometry_data_async(Ref<NavigationMesh baking_navmeshes.insert(p_navigation_mesh); baking_navmesh_mutex.unlock(); - generator_task_mutex.lock(); + MutexLock generator_task_lock(generator_task_mutex); NavMeshGeneratorTask3D *generator_task = memnew(NavMeshGeneratorTask3D); generator_task->navigation_mesh = p_navigation_mesh; generator_task->source_geometry_data = p_source_geometry_data; @@ -234,14 +232,11 @@ void NavMeshGenerator3D::bake_from_source_geometry_data_async(Ref<NavigationMesh generator_task->status = NavMeshGeneratorTask3D::TaskStatus::BAKING_STARTED; generator_task->thread_task_id = WorkerThreadPool::get_singleton()->add_native_task(&NavMeshGenerator3D::generator_thread_bake, generator_task, NavMeshGenerator3D::baking_use_high_priority_threads, SNAME("NavMeshGeneratorBake3D")); generator_tasks.insert(generator_task->thread_task_id, generator_task); - generator_task_mutex.unlock(); } bool NavMeshGenerator3D::is_baking(Ref<NavigationMesh> p_navigation_mesh) { - baking_navmesh_mutex.lock(); - bool baking = baking_navmeshes.has(p_navigation_mesh); - baking_navmesh_mutex.unlock(); - return baking; + MutexLock baking_navmesh_lock(baking_navmesh_mutex); + return baking_navmeshes.has(p_navigation_mesh); } void NavMeshGenerator3D::generator_thread_bake(void *p_arg) { diff --git a/modules/navigation/nav_map.cpp b/modules/navigation/nav_map.cpp index 0c91e8dea3..f89f5b5812 100644 --- a/modules/navigation/nav_map.cpp +++ b/modules/navigation/nav_map.cpp @@ -221,27 +221,27 @@ Vector<Vector3> NavMap::get_path(Vector3 p_origin, Vector3 p_destination, bool p // List of all reachable navigation polys. LocalVector<gd::NavigationPoly> navigation_polys; - navigation_polys.reserve(polygons.size() * 0.75); + navigation_polys.resize(polygons.size() + link_polygons.size()); - // Add the start polygon to the reachable navigation polygons. - gd::NavigationPoly begin_navigation_poly = gd::NavigationPoly(begin_poly); - begin_navigation_poly.self_id = 0; + // Initialize the matching navigation polygon. + gd::NavigationPoly &begin_navigation_poly = navigation_polys[begin_poly->id]; + begin_navigation_poly.poly = begin_poly; begin_navigation_poly.entry = begin_point; begin_navigation_poly.back_navigation_edge_pathway_start = begin_point; begin_navigation_poly.back_navigation_edge_pathway_end = begin_point; - navigation_polys.push_back(begin_navigation_poly); - // List of polygon IDs to visit. - List<uint32_t> to_visit; - to_visit.push_back(0); + // Heap of polygons to travel next. + gd::Heap<gd::NavigationPoly *, gd::NavPolyTravelCostGreaterThan, gd::NavPolyHeapIndexer> + traversable_polys; + traversable_polys.reserve(polygons.size() * 0.25); // This is an implementation of the A* algorithm. - int least_cost_id = 0; + int least_cost_id = begin_poly->id; int prev_least_cost_id = -1; bool found_route = false; const gd::Polygon *reachable_end = nullptr; - real_t reachable_d = FLT_MAX; + real_t distance_to_reachable_end = FLT_MAX; bool is_reachable = true; while (true) { @@ -260,51 +260,57 @@ Vector<Vector3> NavMap::get_path(Vector3 p_origin, Vector3 p_destination, bool p real_t poly_enter_cost = 0.0; real_t poly_travel_cost = least_cost_poly.poly->owner->get_travel_cost(); - if (prev_least_cost_id != -1 && (navigation_polys[prev_least_cost_id].poly->owner->get_self() != least_cost_poly.poly->owner->get_self())) { + if (prev_least_cost_id != -1 && navigation_polys[prev_least_cost_id].poly->owner->get_self() != least_cost_poly.poly->owner->get_self()) { poly_enter_cost = least_cost_poly.poly->owner->get_enter_cost(); } prev_least_cost_id = least_cost_id; Vector3 pathway[2] = { connection.pathway_start, connection.pathway_end }; const Vector3 new_entry = Geometry3D::get_closest_point_to_segment(least_cost_poly.entry, pathway); - const real_t new_distance = (least_cost_poly.entry.distance_to(new_entry) * poly_travel_cost) + poly_enter_cost + least_cost_poly.traveled_distance; - - int64_t already_visited_polygon_index = navigation_polys.find(gd::NavigationPoly(connection.polygon)); - - if (already_visited_polygon_index != -1) { - // Polygon already visited, check if we can reduce the travel cost. - gd::NavigationPoly &avp = navigation_polys[already_visited_polygon_index]; - if (new_distance < avp.traveled_distance) { - avp.back_navigation_poly_id = least_cost_id; - avp.back_navigation_edge = connection.edge; - avp.back_navigation_edge_pathway_start = connection.pathway_start; - avp.back_navigation_edge_pathway_end = connection.pathway_end; - avp.traveled_distance = new_distance; - avp.entry = new_entry; + const real_t new_traveled_distance = least_cost_poly.entry.distance_to(new_entry) * poly_travel_cost + poly_enter_cost + least_cost_poly.traveled_distance; + + // Check if the neighbor polygon has already been processed. + gd::NavigationPoly &neighbor_poly = navigation_polys[connection.polygon->id]; + if (neighbor_poly.poly != nullptr) { + // If the neighbor polygon hasn't been traversed yet and the new path leading to + // it is shorter, update the polygon. + if (neighbor_poly.traversable_poly_index < traversable_polys.size() && + new_traveled_distance < neighbor_poly.traveled_distance) { + neighbor_poly.back_navigation_poly_id = least_cost_id; + neighbor_poly.back_navigation_edge = connection.edge; + neighbor_poly.back_navigation_edge_pathway_start = connection.pathway_start; + neighbor_poly.back_navigation_edge_pathway_end = connection.pathway_end; + neighbor_poly.traveled_distance = new_traveled_distance; + neighbor_poly.distance_to_destination = + new_entry.distance_to(end_point) * + neighbor_poly.poly->owner->get_travel_cost(); + neighbor_poly.entry = new_entry; + + // Update the priority of the polygon in the heap. + traversable_polys.shift(neighbor_poly.traversable_poly_index); } } else { - // Add the neighbor polygon to the reachable ones. - gd::NavigationPoly new_navigation_poly = gd::NavigationPoly(connection.polygon); - new_navigation_poly.self_id = navigation_polys.size(); - new_navigation_poly.back_navigation_poly_id = least_cost_id; - new_navigation_poly.back_navigation_edge = connection.edge; - new_navigation_poly.back_navigation_edge_pathway_start = connection.pathway_start; - new_navigation_poly.back_navigation_edge_pathway_end = connection.pathway_end; - new_navigation_poly.traveled_distance = new_distance; - new_navigation_poly.entry = new_entry; - navigation_polys.push_back(new_navigation_poly); - - // Add the neighbor polygon to the polygons to visit. - to_visit.push_back(navigation_polys.size() - 1); + // Initialize the matching navigation polygon. + neighbor_poly.poly = connection.polygon; + neighbor_poly.back_navigation_poly_id = least_cost_id; + neighbor_poly.back_navigation_edge = connection.edge; + neighbor_poly.back_navigation_edge_pathway_start = connection.pathway_start; + neighbor_poly.back_navigation_edge_pathway_end = connection.pathway_end; + neighbor_poly.traveled_distance = new_traveled_distance; + neighbor_poly.distance_to_destination = + new_entry.distance_to(end_point) * + neighbor_poly.poly->owner->get_travel_cost(); + neighbor_poly.entry = new_entry; + + // Add the polygon to the heap of polygons to traverse next. + traversable_polys.push(&neighbor_poly); } } } - // Removes the least cost polygon from the list of polygons to visit so we can advance. - to_visit.erase(least_cost_id); - - // When the list of polygons to visit is empty at this point it means the End Polygon is not reachable - if (to_visit.size() == 0) { + // When the heap of traversable polygons is empty at this point it means the end polygon is + // unreachable. + if (traversable_polys.is_empty()) { // Thus use the further reachable polygon ERR_BREAK_MSG(is_reachable == false, "It's not expect to not find the most reachable polygons"); is_reachable = false; @@ -366,13 +372,12 @@ Vector<Vector3> NavMap::get_path(Vector3 p_origin, Vector3 p_destination, bool p return path; } - // Reset open and navigation_polys - gd::NavigationPoly np = navigation_polys[0]; - navigation_polys.clear(); - navigation_polys.push_back(np); - to_visit.clear(); - to_visit.push_back(0); - least_cost_id = 0; + for (gd::NavigationPoly &nav_poly : navigation_polys) { + nav_poly.poly = nullptr; + } + navigation_polys[begin_poly->id].poly = begin_poly; + + least_cost_id = begin_poly->id; prev_least_cost_id = -1; reachable_end = nullptr; @@ -380,26 +385,14 @@ Vector<Vector3> NavMap::get_path(Vector3 p_origin, Vector3 p_destination, bool p continue; } - // Find the polygon with the minimum cost from the list of polygons to visit. - least_cost_id = -1; - real_t least_cost = FLT_MAX; - for (List<uint32_t>::Element *element = to_visit.front(); element != nullptr; element = element->next()) { - gd::NavigationPoly *np = &navigation_polys[element->get()]; - real_t cost = np->traveled_distance; - cost += (np->entry.distance_to(end_point) * np->poly->owner->get_travel_cost()); - if (cost < least_cost) { - least_cost_id = np->self_id; - least_cost = cost; - } - } - - ERR_BREAK(least_cost_id == -1); + // Pop the polygon with the lowest travel cost from the heap of traversable polygons. + least_cost_id = traversable_polys.pop()->poly->id; - // Stores the further reachable end polygon, in case our goal is not reachable. + // Store the farthest reachable end polygon in case our goal is not reachable. if (is_reachable) { - real_t d = navigation_polys[least_cost_id].entry.distance_to(p_destination); - if (reachable_d > d) { - reachable_d = d; + real_t distance = navigation_polys[least_cost_id].entry.distance_to(p_destination); + if (distance_to_reachable_end > distance) { + distance_to_reachable_end = distance; reachable_end = navigation_polys[least_cost_id].poly; } } @@ -943,29 +936,30 @@ void NavMap::sync() { } // Resize the polygon count. - int count = 0; + int polygon_count = 0; for (const NavRegion *region : regions) { if (!region->get_enabled()) { continue; } - count += region->get_polygons().size(); + polygon_count += region->get_polygons().size(); } - polygons.resize(count); + polygons.resize(polygon_count); // Copy all region polygons in the map. - count = 0; + polygon_count = 0; for (const NavRegion *region : regions) { if (!region->get_enabled()) { continue; } const LocalVector<gd::Polygon> &polygons_source = region->get_polygons(); for (uint32_t n = 0; n < polygons_source.size(); n++) { - polygons[count + n] = polygons_source[n]; + polygons[polygon_count] = polygons_source[n]; + polygons[polygon_count].id = polygon_count; + polygon_count++; } - count += region->get_polygons().size(); } - _new_pm_polygon_count = polygons.size(); + _new_pm_polygon_count = polygon_count; // Group all edges per key. HashMap<gd::EdgeKey, Vector<gd::Edge::Connection>, gd::EdgeKey> connections; @@ -1136,6 +1130,7 @@ void NavMap::sync() { // If we have both a start and end point, then create a synthetic polygon to route through. if (closest_start_polygon && closest_end_polygon) { gd::Polygon &new_polygon = link_polygons[link_poly_idx++]; + new_polygon.id = polygon_count++; new_polygon.owner = link; new_polygon.edges.clear(); diff --git a/modules/navigation/nav_map.h b/modules/navigation/nav_map.h index 82e8854b7a..c18ee7155b 100644 --- a/modules/navigation/nav_map.h +++ b/modules/navigation/nav_map.h @@ -36,6 +36,7 @@ #include "core/math/math_defs.h" #include "core/object/worker_thread_pool.h" +#include "servers/navigation/navigation_globals.h" #include <KdTree2d.h> #include <KdTree3d.h> @@ -55,21 +56,21 @@ class NavMap : public NavRid { /// To find the polygons edges the vertices are displaced in a grid where /// each cell has the following cell_size and cell_height. - real_t cell_size = 0.25; // Must match ProjectSettings default 3D cell_size and NavigationMesh cell_size. - real_t cell_height = 0.25; // Must match ProjectSettings default 3D cell_height and NavigationMesh cell_height. + real_t cell_size = NavigationDefaults3D::navmesh_cell_size; + real_t cell_height = NavigationDefaults3D::navmesh_cell_height; // For the inter-region merging to work, internal rasterization is performed. - float merge_rasterizer_cell_size = 0.25; - float merge_rasterizer_cell_height = 0.25; + float merge_rasterizer_cell_size = NavigationDefaults3D::navmesh_cell_size; + float merge_rasterizer_cell_height = NavigationDefaults3D::navmesh_cell_height; // This value is used to control sensitivity of internal rasterizer. float merge_rasterizer_cell_scale = 1.0; bool use_edge_connections = true; /// This value is used to detect the near edges to connect. - real_t edge_connection_margin = 0.25; + real_t edge_connection_margin = NavigationDefaults3D::edge_connection_margin; /// This value is used to limit how far links search to find polygons to connect to. - real_t link_connection_radius = 1.0; + real_t link_connection_radius = NavigationDefaults3D::link_connection_radius; bool regenerate_polygons = true; bool regenerate_links = true; diff --git a/modules/navigation/nav_utils.h b/modules/navigation/nav_utils.h index c3939e9979..ba4c44b748 100644 --- a/modules/navigation/nav_utils.h +++ b/modules/navigation/nav_utils.h @@ -98,6 +98,9 @@ struct Edge { }; struct Polygon { + /// Id of the polygon in the map. + uint32_t id = UINT32_MAX; + /// Navigation region or link that contains this polygon. const NavBase *owner = nullptr; @@ -111,9 +114,11 @@ struct Polygon { }; struct NavigationPoly { - uint32_t self_id = 0; /// This poly. - const Polygon *poly; + const Polygon *poly = nullptr; + + /// Index in the heap of traversable polygons. + uint32_t traversable_poly_index = UINT32_MAX; /// Those 4 variables are used to travel the path backwards. int back_navigation_poly_id = -1; @@ -123,20 +128,44 @@ struct NavigationPoly { /// The entry position of this poly. Vector3 entry; - /// The distance to the destination. + /// The distance traveled until now (g cost). real_t traveled_distance = 0.0; + /// The distance to the destination (h cost). + real_t distance_to_destination = 0.0; - NavigationPoly() { poly = nullptr; } + /// The total travel cost (f cost). + real_t total_travel_cost() const { + return traveled_distance + distance_to_destination; + } - NavigationPoly(const Polygon *p_poly) : - poly(p_poly) {} + bool operator==(const NavigationPoly &p_other) const { + return poly == p_other.poly; + } - bool operator==(const NavigationPoly &other) const { - return poly == other.poly; + bool operator!=(const NavigationPoly &p_other) const { + return !(*this == p_other); } +}; + +struct NavPolyTravelCostGreaterThan { + // Returns `true` if the travel cost of `a` is higher than that of `b`. + bool operator()(const NavigationPoly *p_poly_a, const NavigationPoly *p_poly_b) const { + real_t f_cost_a = p_poly_a->total_travel_cost(); + real_t h_cost_a = p_poly_a->distance_to_destination; + real_t f_cost_b = p_poly_b->total_travel_cost(); + real_t h_cost_b = p_poly_b->distance_to_destination; - bool operator!=(const NavigationPoly &other) const { - return !operator==(other); + if (f_cost_a != f_cost_b) { + return f_cost_a > f_cost_b; + } else { + return h_cost_a > h_cost_b; + } + } +}; + +struct NavPolyHeapIndexer { + void operator()(NavigationPoly *p_poly, uint32_t p_heap_index) const { + p_poly->traversable_poly_index = p_heap_index; } }; @@ -146,6 +175,129 @@ struct ClosestPointQueryResult { RID owner; }; +template <typename T> +struct NoopIndexer { + void operator()(const T &p_value, uint32_t p_index) {} +}; + +/** + * A max-heap implementation that notifies of element index changes. + */ +template <typename T, typename LessThan = Comparator<T>, typename Indexer = NoopIndexer<T>> +class Heap { + LocalVector<T> _buffer; + + LessThan _less_than; + Indexer _indexer; + +public: + void reserve(uint32_t p_size) { + _buffer.reserve(p_size); + } + + uint32_t size() const { + return _buffer.size(); + } + + bool is_empty() const { + return _buffer.is_empty(); + } + + void push(const T &p_element) { + _buffer.push_back(p_element); + _indexer(p_element, _buffer.size() - 1); + _shift_up(_buffer.size() - 1); + } + + T pop() { + ERR_FAIL_COND_V_MSG(_buffer.is_empty(), T(), "Can't pop an empty heap."); + T value = _buffer[0]; + _indexer(value, UINT32_MAX); + if (_buffer.size() > 1) { + _buffer[0] = _buffer[_buffer.size() - 1]; + _indexer(_buffer[0], 0); + _buffer.remove_at(_buffer.size() - 1); + _shift_down(0); + } else { + _buffer.remove_at(_buffer.size() - 1); + } + return value; + } + + /** + * Update the position of the element in the heap if necessary. + */ + void shift(uint32_t p_index) { + ERR_FAIL_UNSIGNED_INDEX_MSG(p_index, _buffer.size(), "Heap element index is out of range."); + if (!_shift_up(p_index)) { + _shift_down(p_index); + } + } + + void clear() { + for (const T &value : _buffer) { + _indexer(value, UINT32_MAX); + } + _buffer.clear(); + } + + Heap() {} + + Heap(const LessThan &p_less_than) : + _less_than(p_less_than) {} + + Heap(const Indexer &p_indexer) : + _indexer(p_indexer) {} + + Heap(const LessThan &p_less_than, const Indexer &p_indexer) : + _less_than(p_less_than), _indexer(p_indexer) {} + +private: + bool _shift_up(uint32_t p_index) { + T value = _buffer[p_index]; + uint32_t current_index = p_index; + uint32_t parent_index = (current_index - 1) / 2; + while (current_index > 0 && _less_than(_buffer[parent_index], value)) { + _buffer[current_index] = _buffer[parent_index]; + _indexer(_buffer[current_index], current_index); + current_index = parent_index; + parent_index = (current_index - 1) / 2; + } + if (current_index != p_index) { + _buffer[current_index] = value; + _indexer(value, current_index); + return true; + } else { + return false; + } + } + + bool _shift_down(uint32_t p_index) { + T value = _buffer[p_index]; + uint32_t current_index = p_index; + uint32_t child_index = 2 * current_index + 1; + while (child_index < _buffer.size()) { + if (child_index + 1 < _buffer.size() && + _less_than(_buffer[child_index], _buffer[child_index + 1])) { + child_index++; + } + if (_less_than(_buffer[child_index], value)) { + break; + } + _buffer[current_index] = _buffer[child_index]; + _indexer(_buffer[current_index], current_index); + current_index = child_index; + child_index = 2 * current_index + 1; + } + if (current_index != p_index) { + _buffer[current_index] = value; + _indexer(value, current_index); + return true; + } else { + return false; + } + } +}; } // namespace gd #endif // NAV_UTILS_H diff --git a/modules/noise/noise_texture_3d.cpp b/modules/noise/noise_texture_3d.cpp index 1e929e6f63..9047491344 100644 --- a/modules/noise/noise_texture_3d.cpp +++ b/modules/noise/noise_texture_3d.cpp @@ -331,6 +331,10 @@ int NoiseTexture3D::get_depth() const { return depth; } +bool NoiseTexture3D::has_mipmaps() const { + return false; +} + RID NoiseTexture3D::get_rid() const { if (!texture.is_valid()) { texture = RS::get_singleton()->texture_3d_placeholder_create(); diff --git a/modules/noise/noise_texture_3d.h b/modules/noise/noise_texture_3d.h index 13125efe7f..d55b78a2ba 100644 --- a/modules/noise/noise_texture_3d.h +++ b/modules/noise/noise_texture_3d.h @@ -103,6 +103,8 @@ public: virtual int get_height() const override; virtual int get_depth() const override; + virtual bool has_mipmaps() const override; + virtual RID get_rid() const override; virtual Vector<Ref<Image>> get_data() const override; diff --git a/modules/openxr/doc_classes/OpenXRAPIExtension.xml b/modules/openxr/doc_classes/OpenXRAPIExtension.xml index 4419d24dd3..432b331eec 100644 --- a/modules/openxr/doc_classes/OpenXRAPIExtension.xml +++ b/modules/openxr/doc_classes/OpenXRAPIExtension.xml @@ -17,12 +17,25 @@ <link title="XrPosef documentation">https://registry.khronos.org/OpenXR/specs/1.0/man/html/XrPosef.html</link> </tutorials> <methods> + <method name="begin_debug_label_region"> + <return type="void" /> + <param index="0" name="label_name" type="String" /> + <description> + Begins a new debug label region, this label will be reported in debug messages for any calls following this until [method end_debug_label_region] is called. Debug labels can be stacked. + </description> + </method> <method name="can_render"> <return type="bool" /> <description> Returns [code]true[/code] if OpenXR is initialized for rendering with an XR viewport. </description> </method> + <method name="end_debug_label_region"> + <return type="void" /> + <description> + Marks the end of a debug label region. Removes the latest debug label region added by calling [method begin_debug_label_region]. + </description> + </method> <method name="get_error_string"> <return type="String" /> <param index="0" name="result" type="int" /> @@ -88,6 +101,13 @@ Returns the id of the system, which is a [url=https://registry.khronos.org/OpenXR/specs/1.0/man/html/XrSystemId.html]XrSystemId[/url] cast to an integer. </description> </method> + <method name="insert_debug_label"> + <return type="void" /> + <param index="0" name="label_name" type="String" /> + <description> + Inserts a debug label, this label is reported in any debug message resulting from the OpenXR calls that follows, until any of [method begin_debug_label_region], [method end_debug_label_region], or [method insert_debug_label] is called. + </description> + </method> <method name="is_environment_blend_mode_alpha_supported"> <return type="int" enum="OpenXRAPIExtension.OpenXRAlphaBlendModeSupport" /> <description> @@ -127,6 +147,15 @@ If set to [code]true[/code], an OpenXR extension is loaded which is capable of emulating the [constant XRInterface.XR_ENV_BLEND_MODE_ALPHA_BLEND] blend mode. </description> </method> + <method name="set_object_name"> + <return type="void" /> + <param index="0" name="object_type" type="int" /> + <param index="1" name="object_handle" type="int" /> + <param index="2" name="object_name" type="String" /> + <description> + Set the object name of an OpenXR object, used for debug output. [param object_type] must be a valid OpenXR [code]XrObjectType[/code] enum and [param object_handle] must be a valid OpenXR object handle. + </description> + </method> <method name="transform_from_pose"> <return type="Transform3D" /> <param index="0" name="pose" type="const void*" /> diff --git a/modules/openxr/extensions/openxr_composition_layer_extension.cpp b/modules/openxr/extensions/openxr_composition_layer_extension.cpp index 994b08af53..8a448afc08 100644 --- a/modules/openxr/extensions/openxr_composition_layer_extension.cpp +++ b/modules/openxr/extensions/openxr_composition_layer_extension.cpp @@ -58,7 +58,7 @@ HashMap<String, bool *> OpenXRCompositionLayerExtension::get_requested_extension return request_extensions; } -void OpenXRCompositionLayerExtension::on_session_created(const XrSession p_instance) { +void OpenXRCompositionLayerExtension::on_session_created(const XrSession p_session) { OpenXRAPI::get_singleton()->register_composition_layer_provider(this); } diff --git a/modules/openxr/extensions/openxr_composition_layer_extension.h b/modules/openxr/extensions/openxr_composition_layer_extension.h index 4fefc416e6..34e330a60a 100644 --- a/modules/openxr/extensions/openxr_composition_layer_extension.h +++ b/modules/openxr/extensions/openxr_composition_layer_extension.h @@ -49,7 +49,7 @@ public: virtual ~OpenXRCompositionLayerExtension() override; virtual HashMap<String, bool *> get_requested_extensions() override; - virtual void on_session_created(const XrSession p_instance) override; + virtual void on_session_created(const XrSession p_session) override; virtual void on_session_destroyed() override; virtual void on_pre_render() override; diff --git a/modules/openxr/extensions/openxr_debug_utils_extension.cpp b/modules/openxr/extensions/openxr_debug_utils_extension.cpp new file mode 100644 index 0000000000..10dbe629f7 --- /dev/null +++ b/modules/openxr/extensions/openxr_debug_utils_extension.cpp @@ -0,0 +1,287 @@ +/**************************************************************************/ +/* openxr_debug_utils_extension.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 "openxr_debug_utils_extension.h" + +#include "../openxr_api.h" +#include "core/config/project_settings.h" +#include "core/string/print_string.h" + +#include <openxr/openxr.h> + +OpenXRDebugUtilsExtension *OpenXRDebugUtilsExtension::singleton = nullptr; + +OpenXRDebugUtilsExtension *OpenXRDebugUtilsExtension::get_singleton() { + return singleton; +} + +OpenXRDebugUtilsExtension::OpenXRDebugUtilsExtension() { + singleton = this; +} + +OpenXRDebugUtilsExtension::~OpenXRDebugUtilsExtension() { + singleton = nullptr; +} + +HashMap<String, bool *> OpenXRDebugUtilsExtension::get_requested_extensions() { + HashMap<String, bool *> request_extensions; + + request_extensions[XR_EXT_DEBUG_UTILS_EXTENSION_NAME] = &debug_utils_ext; + + return request_extensions; +} + +void OpenXRDebugUtilsExtension::on_instance_created(const XrInstance p_instance) { + if (debug_utils_ext) { + EXT_INIT_XR_FUNC(xrCreateDebugUtilsMessengerEXT); + EXT_INIT_XR_FUNC(xrDestroyDebugUtilsMessengerEXT); + EXT_INIT_XR_FUNC(xrSetDebugUtilsObjectNameEXT); + EXT_INIT_XR_FUNC(xrSessionBeginDebugUtilsLabelRegionEXT); + EXT_INIT_XR_FUNC(xrSessionEndDebugUtilsLabelRegionEXT); + EXT_INIT_XR_FUNC(xrSessionInsertDebugUtilsLabelEXT); + + debug_utils_ext = xrCreateDebugUtilsMessengerEXT_ptr && xrDestroyDebugUtilsMessengerEXT_ptr && xrSetDebugUtilsObjectNameEXT_ptr && xrSessionBeginDebugUtilsLabelRegionEXT_ptr && xrSessionEndDebugUtilsLabelRegionEXT_ptr && xrSessionInsertDebugUtilsLabelEXT_ptr; + } else { + WARN_PRINT("OpenXR: The debug utils extension is not available on this runtime. Debug logging is not enabled!"); + } + + // On successful init, setup our default messenger. + if (debug_utils_ext) { + int max_severity = GLOBAL_GET("xr/openxr/extensions/debug_utils"); + int types = GLOBAL_GET("xr/openxr/extensions/debug_message_types"); + + XrDebugUtilsMessageSeverityFlagsEXT message_severities = 0; + + if (max_severity >= 1) { + message_severities |= XR_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT; + } + if (max_severity >= 2) { + message_severities |= XR_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT; + } + if (max_severity >= 3) { + message_severities |= XR_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT; + } + if (max_severity >= 4) { + message_severities |= XR_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT; + } + + XrDebugUtilsMessageTypeFlagsEXT message_types = 0; + + // These should match up but just to be safe and future proof... + if (types & 1) { + message_types |= XR_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT; + } + if (types & 2) { + message_types |= XR_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT; + } + if (types & 4) { + message_types |= XR_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT; + } + if (types & 8) { + message_types |= XR_DEBUG_UTILS_MESSAGE_TYPE_CONFORMANCE_BIT_EXT; + } + + XrDebugUtilsMessengerCreateInfoEXT callback_info = { + XR_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT, // type + nullptr, // next + message_severities, // messageSeverities + message_types, // messageTypes + &OpenXRDebugUtilsExtension::_debug_callback, // userCallback + nullptr, // userData + }; + + XrResult result = xrCreateDebugUtilsMessengerEXT(p_instance, &callback_info, &default_messenger); + if (XR_FAILED(result)) { + ERR_PRINT("OpenXR: Failed to create debug callback [" + OpenXRAPI::get_singleton()->get_error_string(result) + "]"); + } + + set_object_name(XR_OBJECT_TYPE_INSTANCE, uint64_t(p_instance), "Main Godot OpenXR Instance"); + } +} + +void OpenXRDebugUtilsExtension::on_instance_destroyed() { + if (default_messenger != XR_NULL_HANDLE) { + XrResult result = xrDestroyDebugUtilsMessengerEXT(default_messenger); + if (XR_FAILED(result)) { + ERR_PRINT("OpenXR: Failed to destroy debug callback [" + OpenXRAPI::get_singleton()->get_error_string(result) + "]"); + } + + default_messenger = XR_NULL_HANDLE; + } + + xrCreateDebugUtilsMessengerEXT_ptr = nullptr; + xrDestroyDebugUtilsMessengerEXT_ptr = nullptr; + xrSetDebugUtilsObjectNameEXT_ptr = nullptr; + xrSessionBeginDebugUtilsLabelRegionEXT_ptr = nullptr; + xrSessionEndDebugUtilsLabelRegionEXT_ptr = nullptr; + xrSessionInsertDebugUtilsLabelEXT_ptr = nullptr; + debug_utils_ext = false; +} + +bool OpenXRDebugUtilsExtension::get_active() { + return debug_utils_ext; +} + +void OpenXRDebugUtilsExtension::set_object_name(XrObjectType p_object_type, uint64_t p_object_handle, const char *p_object_name) { + ERR_FAIL_COND(!debug_utils_ext); + ERR_FAIL_NULL(xrSetDebugUtilsObjectNameEXT_ptr); + + const XrDebugUtilsObjectNameInfoEXT space_name_info = { + XR_TYPE_DEBUG_UTILS_OBJECT_NAME_INFO_EXT, // type + nullptr, // next + p_object_type, // objectType + p_object_handle, // objectHandle + p_object_name, // objectName + }; + + XrResult result = xrSetDebugUtilsObjectNameEXT_ptr(OpenXRAPI::get_singleton()->get_instance(), &space_name_info); + if (XR_FAILED(result)) { + ERR_PRINT("OpenXR: Failed to set object name [" + OpenXRAPI::get_singleton()->get_error_string(result) + "]"); + } +} + +void OpenXRDebugUtilsExtension::begin_debug_label_region(const char *p_label_name) { + ERR_FAIL_COND(!debug_utils_ext); + ERR_FAIL_NULL(xrSessionBeginDebugUtilsLabelRegionEXT_ptr); + + const XrDebugUtilsLabelEXT session_active_region_label = { + XR_TYPE_DEBUG_UTILS_LABEL_EXT, // type + NULL, // next + p_label_name, // labelName + }; + + XrResult result = xrSessionBeginDebugUtilsLabelRegionEXT_ptr(OpenXRAPI::get_singleton()->get_session(), &session_active_region_label); + if (XR_FAILED(result)) { + ERR_PRINT("OpenXR: Failed to begin label region [" + OpenXRAPI::get_singleton()->get_error_string(result) + "]"); + } +} + +void OpenXRDebugUtilsExtension::end_debug_label_region() { + ERR_FAIL_COND(!debug_utils_ext); + ERR_FAIL_NULL(xrSessionEndDebugUtilsLabelRegionEXT_ptr); + + XrResult result = xrSessionEndDebugUtilsLabelRegionEXT_ptr(OpenXRAPI::get_singleton()->get_session()); + if (XR_FAILED(result)) { + ERR_PRINT("OpenXR: Failed to end label region [" + OpenXRAPI::get_singleton()->get_error_string(result) + "]"); + } +} + +void OpenXRDebugUtilsExtension::insert_debug_label(const char *p_label_name) { + ERR_FAIL_COND(!debug_utils_ext); + ERR_FAIL_NULL(xrSessionInsertDebugUtilsLabelEXT_ptr); + + const XrDebugUtilsLabelEXT session_active_region_label = { + XR_TYPE_DEBUG_UTILS_LABEL_EXT, // type + NULL, // next + p_label_name, // labelName + }; + + XrResult result = xrSessionInsertDebugUtilsLabelEXT_ptr(OpenXRAPI::get_singleton()->get_session(), &session_active_region_label); + if (XR_FAILED(result)) { + ERR_PRINT("OpenXR: Failed to insert label [" + OpenXRAPI::get_singleton()->get_error_string(result) + "]"); + } +} + +XrBool32 XRAPI_PTR OpenXRDebugUtilsExtension::_debug_callback(XrDebugUtilsMessageSeverityFlagsEXT p_message_severity, XrDebugUtilsMessageTypeFlagsEXT p_message_types, const XrDebugUtilsMessengerCallbackDataEXT *p_callback_data, void *p_user_data) { + OpenXRDebugUtilsExtension *debug_utils = OpenXRDebugUtilsExtension::get_singleton(); + + if (debug_utils) { + return debug_utils->debug_callback(p_message_severity, p_message_types, p_callback_data, p_user_data); + } + + return XR_FALSE; +} + +XrBool32 OpenXRDebugUtilsExtension::debug_callback(XrDebugUtilsMessageSeverityFlagsEXT p_message_severity, XrDebugUtilsMessageTypeFlagsEXT p_message_types, const XrDebugUtilsMessengerCallbackDataEXT *p_callback_data, void *p_user_data) { + String msg; + + ERR_FAIL_NULL_V(p_callback_data, XR_FALSE); + + if (p_message_types == XR_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT) { + msg = ", type: General"; + } else if (p_message_types == XR_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT) { + msg = ", type: Validation"; + } else if (p_message_types == XR_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT) { + msg = ", type: Performance"; + } else if (p_message_types == XR_DEBUG_UTILS_MESSAGE_TYPE_CONFORMANCE_BIT_EXT) { + msg = ", type: Conformance"; + } else { + msg = ", type: Unknown (" + String::num_uint64(p_message_types) + ")"; + } + + if (p_callback_data->functionName) { + msg += ", function Name: " + String(p_callback_data->functionName); + } + if (p_callback_data->messageId) { + msg += "\nMessage ID: " + String(p_callback_data->messageId); + } + if (p_callback_data->message) { + msg += "\nMessage: " + String(p_callback_data->message); + } + + if (p_callback_data->objectCount > 0) { + String objects; + + for (uint32_t i = 0; i < p_callback_data->objectCount; i++) { + if (!objects.is_empty()) { + objects += ", "; + } + objects += p_callback_data->objects[i].objectName; + } + + msg += "\nObjects: " + objects; + } + + if (p_callback_data->sessionLabelCount > 0) { + String labels; + + for (uint32_t i = 0; i < p_callback_data->sessionLabelCount; i++) { + if (!labels.is_empty()) { + labels += ", "; + } + labels += p_callback_data->sessionLabels[i].labelName; + } + + msg += "\nLabels: " + labels; + } + + if (p_message_severity == XR_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT) { + ERR_PRINT("OpenXR: Severity: Error" + msg); + } else if (p_message_severity == XR_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT) { + WARN_PRINT("OpenXR: Severity: Warning" + msg); + } else if (p_message_severity == XR_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT) { + print_line("OpenXR: Severity: Info" + msg); + } else if (p_message_severity == XR_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT) { + // This is a bit double because we won't output this unless verbose messaging in Godot is on. + print_verbose("OpenXR: Severity: Verbose" + msg); + } + + return XR_FALSE; +} diff --git a/modules/openxr/extensions/openxr_debug_utils_extension.h b/modules/openxr/extensions/openxr_debug_utils_extension.h new file mode 100644 index 0000000000..1ee4c2a101 --- /dev/null +++ b/modules/openxr/extensions/openxr_debug_utils_extension.h @@ -0,0 +1,76 @@ +/**************************************************************************/ +/* openxr_debug_utils_extension.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. */ +/**************************************************************************/ + +#ifndef OPENXR_DEBUG_UTILS_EXTENSION_H +#define OPENXR_DEBUG_UTILS_EXTENSION_H + +#include "../util.h" +#include "openxr_extension_wrapper.h" + +class OpenXRDebugUtilsExtension : public OpenXRExtensionWrapper { +public: + static OpenXRDebugUtilsExtension *get_singleton(); + + OpenXRDebugUtilsExtension(); + virtual ~OpenXRDebugUtilsExtension() override; + + virtual HashMap<String, bool *> get_requested_extensions() override; + virtual void on_instance_created(const XrInstance p_instance) override; + virtual void on_instance_destroyed() override; + + bool get_active(); + + void set_object_name(XrObjectType p_object_type, uint64_t p_object_handle, const char *p_object_name); + void begin_debug_label_region(const char *p_label_name); + void end_debug_label_region(); + void insert_debug_label(const char *p_label_name); + +private: + static OpenXRDebugUtilsExtension *singleton; + + // related extensions + bool debug_utils_ext = false; + + // debug handlers + XrDebugUtilsMessengerEXT default_messenger = XR_NULL_HANDLE; + + static XrBool32 XRAPI_PTR _debug_callback(XrDebugUtilsMessageSeverityFlagsEXT p_message_severity, XrDebugUtilsMessageTypeFlagsEXT p_message_types, const XrDebugUtilsMessengerCallbackDataEXT *p_callback_data, void *p_user_data); + XrBool32 debug_callback(XrDebugUtilsMessageSeverityFlagsEXT p_message_severity, XrDebugUtilsMessageTypeFlagsEXT p_message_types, const XrDebugUtilsMessengerCallbackDataEXT *p_callback_data, void *p_user_data); + + // OpenXR API call wrappers + EXT_PROTO_XRRESULT_FUNC3(xrCreateDebugUtilsMessengerEXT, (XrInstance), p_instance, (const XrDebugUtilsMessengerCreateInfoEXT *), p_create_info, (XrDebugUtilsMessengerEXT *), p_messenger) + EXT_PROTO_XRRESULT_FUNC1(xrDestroyDebugUtilsMessengerEXT, (XrDebugUtilsMessengerEXT), p_messenger) + EXT_PROTO_XRRESULT_FUNC2(xrSetDebugUtilsObjectNameEXT, (XrInstance), p_instance, (const XrDebugUtilsObjectNameInfoEXT *), p_name_info) + EXT_PROTO_XRRESULT_FUNC2(xrSessionBeginDebugUtilsLabelRegionEXT, (XrSession), p_session, (const XrDebugUtilsLabelEXT *), p_label_info) + EXT_PROTO_XRRESULT_FUNC1(xrSessionEndDebugUtilsLabelRegionEXT, (XrSession), p_session) + EXT_PROTO_XRRESULT_FUNC2(xrSessionInsertDebugUtilsLabelEXT, (XrSession), p_session, (const XrDebugUtilsLabelEXT *), p_label_info) +}; + +#endif // OPENXR_DEBUG_UTILS_EXTENSION_H diff --git a/modules/openxr/extensions/openxr_extension_wrapper.h b/modules/openxr/extensions/openxr_extension_wrapper.h index 8d05657afc..09a9556dfa 100644 --- a/modules/openxr/extensions/openxr_extension_wrapper.h +++ b/modules/openxr/extensions/openxr_extension_wrapper.h @@ -76,7 +76,7 @@ public: virtual void on_before_instance_created() {} // `on_before_instance_created` is called before we create our OpenXR instance. virtual void on_instance_created(const XrInstance p_instance) {} // `on_instance_created` is called right after we've successfully created our OpenXR instance. virtual void on_instance_destroyed() {} // `on_instance_destroyed` is called right before we destroy our OpenXR instance. - virtual void on_session_created(const XrSession p_instance) {} // `on_session_created` is called right after we've successfully created our OpenXR session. + virtual void on_session_created(const XrSession p_session) {} // `on_session_created` is called right after we've successfully created our OpenXR session. virtual void on_session_destroyed() {} // `on_session_destroyed` is called right before we destroy our OpenXR session. // `on_process` is called as part of our OpenXR process handling, diff --git a/modules/openxr/openxr_api.cpp b/modules/openxr/openxr_api.cpp index e4ec318a42..ecf7c05789 100644 --- a/modules/openxr/openxr_api.cpp +++ b/modules/openxr/openxr_api.cpp @@ -54,6 +54,7 @@ #endif #include "extensions/openxr_composition_layer_depth_extension.h" +#include "extensions/openxr_debug_utils_extension.h" #include "extensions/openxr_eye_gaze_interaction.h" #include "extensions/openxr_fb_display_refresh_rate_extension.h" #include "extensions/openxr_fb_foveation_extension.h" @@ -316,6 +317,46 @@ String OpenXRAPI::get_swapchain_format_name(int64_t p_swapchain_format) const { return String("Swapchain format ") + String::num_int64(int64_t(p_swapchain_format)); } +void OpenXRAPI::set_object_name(XrObjectType p_object_type, uint64_t p_object_handle, const String &p_object_name) { + OpenXRDebugUtilsExtension *debug_utils = OpenXRDebugUtilsExtension::get_singleton(); + if (!debug_utils || !debug_utils->get_active()) { + // Not enabled/active? Ignore. + return; + } + + debug_utils->set_object_name(p_object_type, p_object_handle, p_object_name.utf8().get_data()); +} + +void OpenXRAPI::begin_debug_label_region(const String &p_label_name) { + OpenXRDebugUtilsExtension *debug_utils = OpenXRDebugUtilsExtension::get_singleton(); + if (!debug_utils || !debug_utils->get_active()) { + // Not enabled/active? Ignore. + return; + } + + debug_utils->begin_debug_label_region(p_label_name.utf8().get_data()); +} + +void OpenXRAPI::end_debug_label_region() { + OpenXRDebugUtilsExtension *debug_utils = OpenXRDebugUtilsExtension::get_singleton(); + if (!debug_utils || !debug_utils->get_active()) { + // Not enabled/active? Ignore. + return; + } + + debug_utils->end_debug_label_region(); +} + +void OpenXRAPI::insert_debug_label(const String &p_label_name) { + OpenXRDebugUtilsExtension *debug_utils = OpenXRDebugUtilsExtension::get_singleton(); + if (!debug_utils || !debug_utils->get_active()) { + // Not enabled/active? Ignore. + return; + } + + debug_utils->insert_debug_label(p_label_name.utf8().get_data()); +} + bool OpenXRAPI::load_layer_properties() { // This queries additional layers that are available and can be initialized when we create our OpenXR instance if (layer_properties != nullptr) { @@ -826,6 +867,10 @@ bool OpenXRAPI::create_session() { return false; } + set_object_name(XR_OBJECT_TYPE_SESSION, uint64_t(session), "Main Godot OpenXR Session"); + + begin_debug_label_region("Godot session active"); + for (OpenXRExtensionWrapper *wrapper : registered_extension_wrappers) { wrapper->on_session_created(session); } @@ -916,6 +961,8 @@ bool OpenXRAPI::setup_play_space() { print_line("OpenXR: Failed to create LOCAL space in order to emulate LOCAL_FLOOR [", get_error_string(result), "]"); will_emulate_local_floor = false; } + + set_object_name(XR_OBJECT_TYPE_SPACE, uint64_t(local_floor_emulation.local_space), "Emulation local space"); } if (local_floor_emulation.stage_space == XR_NULL_HANDLE) { @@ -931,6 +978,8 @@ bool OpenXRAPI::setup_play_space() { print_line("OpenXR: Failed to create STAGE space in order to emulate LOCAL_FLOOR [", get_error_string(result), "]"); will_emulate_local_floor = false; } + + set_object_name(XR_OBJECT_TYPE_SPACE, uint64_t(local_floor_emulation.stage_space), "Emulation stage space"); } if (!will_emulate_local_floor) { @@ -972,6 +1021,8 @@ bool OpenXRAPI::setup_play_space() { play_space = new_play_space; reference_space = new_reference_space; + set_object_name(XR_OBJECT_TYPE_SPACE, uint64_t(play_space), "Play space"); + local_floor_emulation.enabled = will_emulate_local_floor; local_floor_emulation.should_reset_floor_height = will_emulate_local_floor; @@ -1007,6 +1058,8 @@ bool OpenXRAPI::setup_view_space() { return false; } + set_object_name(XR_OBJECT_TYPE_SPACE, uint64_t(view_space), "View space"); + return true; } @@ -1181,6 +1234,8 @@ bool OpenXRAPI::create_main_swapchains(Size2i p_size) { if (!render_state.main_swapchains[OPENXR_SWAPCHAIN_COLOR].create(0, XR_SWAPCHAIN_USAGE_SAMPLED_BIT | XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT | XR_SWAPCHAIN_USAGE_MUTABLE_FORMAT_BIT, color_swapchain_format, render_state.main_swapchain_size.width, render_state.main_swapchain_size.height, sample_count, view_count)) { return false; } + + set_object_name(XR_OBJECT_TYPE_SWAPCHAIN, uint64_t(render_state.main_swapchains[OPENXR_SWAPCHAIN_COLOR].get_swapchain()), "Main color swapchain"); } // We create our depth swapchain if: @@ -1191,6 +1246,8 @@ bool OpenXRAPI::create_main_swapchains(Size2i p_size) { if (!render_state.main_swapchains[OPENXR_SWAPCHAIN_DEPTH].create(0, XR_SWAPCHAIN_USAGE_SAMPLED_BIT | XR_SWAPCHAIN_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT, depth_swapchain_format, render_state.main_swapchain_size.width, render_state.main_swapchain_size.height, sample_count, view_count)) { return false; } + + set_object_name(XR_OBJECT_TYPE_SWAPCHAIN, uint64_t(render_state.main_swapchains[OPENXR_SWAPCHAIN_COLOR].get_swapchain()), "Main depth swapchain"); } // We create our velocity swapchain if: @@ -1309,6 +1366,8 @@ void OpenXRAPI::destroy_session() { wrapper->on_session_destroyed(); } + end_debug_label_region(); + xrDestroySession(session); session = XR_NULL_HANDLE; } @@ -2215,6 +2274,9 @@ void OpenXRAPI::pre_render() { } } + // We should get our frame no from the rendering server, but this will do. + begin_debug_label_region(String("Session Frame ") + String::num_uint64(++render_state.frame)); + // let's start our frame.. XrFrameBeginInfo frame_begin_info = { XR_TYPE_FRAME_BEGIN_INFO, // type @@ -2333,6 +2395,8 @@ void OpenXRAPI::end_frame() { return; } + end_debug_label_region(); // Session frame # + // neither eye is rendered return; } @@ -2407,6 +2471,8 @@ void OpenXRAPI::end_frame() { print_line("OpenXR: failed to end frame! [", get_error_string(result), "]"); return; } + + end_debug_label_region(); // Session frame # } float OpenXRAPI::get_display_refresh_rate() const { @@ -2822,6 +2888,8 @@ RID OpenXRAPI::action_set_create(const String p_name, const String p_localized_n return RID(); } + set_object_name(XR_OBJECT_TYPE_ACTION_SET, uint64_t(action_set.handle), p_name); + return action_set_owner.make_rid(action_set); } @@ -2997,6 +3065,8 @@ RID OpenXRAPI::action_create(RID p_action_set, const String p_name, const String return RID(); } + set_object_name(XR_OBJECT_TYPE_ACTION, uint64_t(action.handle), p_name); + return action_owner.make_rid(action); } diff --git a/modules/openxr/openxr_api.h b/modules/openxr/openxr_api.h index 88455379b9..0d1e4eb414 100644 --- a/modules/openxr/openxr_api.h +++ b/modules/openxr/openxr_api.h @@ -336,6 +336,7 @@ private: XrTime predicted_display_time = 0; XrSpace play_space = XR_NULL_HANDLE; double render_target_size_multiplier = 1.0; + uint64_t frame = 0; uint32_t view_count = 0; XrView *views = nullptr; @@ -422,6 +423,10 @@ public: XrResult get_instance_proc_addr(const char *p_name, PFN_xrVoidFunction *p_addr); String get_error_string(XrResult result) const; String get_swapchain_format_name(int64_t p_swapchain_format) const; + void set_object_name(XrObjectType p_object_type, uint64_t p_object_handle, const String &p_object_name); + void begin_debug_label_region(const String &p_label_name); + void end_debug_label_region(); + void insert_debug_label(const String &p_label_name); OpenXRInterface *get_xr_interface() const { return xr_interface; } void set_xr_interface(OpenXRInterface *p_xr_interface); diff --git a/modules/openxr/openxr_api_extension.cpp b/modules/openxr/openxr_api_extension.cpp index a1744fa1db..f3bc178d3a 100644 --- a/modules/openxr/openxr_api_extension.cpp +++ b/modules/openxr/openxr_api_extension.cpp @@ -43,6 +43,10 @@ void OpenXRAPIExtension::_bind_methods() { ClassDB::bind_method(D_METHOD("get_instance_proc_addr", "name"), &OpenXRAPIExtension::get_instance_proc_addr); ClassDB::bind_method(D_METHOD("get_error_string", "result"), &OpenXRAPIExtension::get_error_string); ClassDB::bind_method(D_METHOD("get_swapchain_format_name", "swapchain_format"), &OpenXRAPIExtension::get_swapchain_format_name); + ClassDB::bind_method(D_METHOD("set_object_name", "object_type", "object_handle", "object_name"), &OpenXRAPIExtension::set_object_name); + ClassDB::bind_method(D_METHOD("begin_debug_label_region", "label_name"), &OpenXRAPIExtension::begin_debug_label_region); + ClassDB::bind_method(D_METHOD("end_debug_label_region"), &OpenXRAPIExtension::end_debug_label_region); + ClassDB::bind_method(D_METHOD("insert_debug_label", "label_name"), &OpenXRAPIExtension::insert_debug_label); ClassDB::bind_method(D_METHOD("is_initialized"), &OpenXRAPIExtension::is_initialized); ClassDB::bind_method(D_METHOD("is_running"), &OpenXRAPIExtension::is_running); @@ -116,6 +120,30 @@ String OpenXRAPIExtension::get_swapchain_format_name(int64_t p_swapchain_format) return OpenXRAPI::get_singleton()->get_swapchain_format_name(p_swapchain_format); } +void OpenXRAPIExtension::set_object_name(int64_t p_object_type, uint64_t p_object_handle, const String &p_object_name) { + ERR_FAIL_NULL(OpenXRAPI::get_singleton()); + + OpenXRAPI::get_singleton()->set_object_name(XrObjectType(p_object_type), p_object_handle, p_object_name); +} + +void OpenXRAPIExtension::begin_debug_label_region(const String &p_label_name) { + ERR_FAIL_NULL(OpenXRAPI::get_singleton()); + + OpenXRAPI::get_singleton()->begin_debug_label_region(p_label_name); +} + +void OpenXRAPIExtension::end_debug_label_region() { + ERR_FAIL_NULL(OpenXRAPI::get_singleton()); + + OpenXRAPI::get_singleton()->end_debug_label_region(); +} + +void OpenXRAPIExtension::insert_debug_label(const String &p_label_name) { + ERR_FAIL_NULL(OpenXRAPI::get_singleton()); + + OpenXRAPI::get_singleton()->insert_debug_label(p_label_name); +} + bool OpenXRAPIExtension::is_initialized() { ERR_FAIL_NULL_V(OpenXRAPI::get_singleton(), false); return OpenXRAPI::get_singleton()->is_initialized(); diff --git a/modules/openxr/openxr_api_extension.h b/modules/openxr/openxr_api_extension.h index cff2c4738e..1b88b418f6 100644 --- a/modules/openxr/openxr_api_extension.h +++ b/modules/openxr/openxr_api_extension.h @@ -64,6 +64,10 @@ public: uint64_t get_instance_proc_addr(String p_name); String get_error_string(uint64_t result); String get_swapchain_format_name(int64_t p_swapchain_format); + void set_object_name(int64_t p_object_type, uint64_t p_object_handle, const String &p_object_name); + void begin_debug_label_region(const String &p_label_name); + void end_debug_label_region(); + void insert_debug_label(const String &p_label_name); bool is_initialized(); bool is_running(); diff --git a/modules/openxr/register_types.cpp b/modules/openxr/register_types.cpp index 61c294eff5..f3fda2517c 100644 --- a/modules/openxr/register_types.cpp +++ b/modules/openxr/register_types.cpp @@ -48,6 +48,7 @@ #include "extensions/openxr_composition_layer_depth_extension.h" #include "extensions/openxr_composition_layer_extension.h" +#include "extensions/openxr_debug_utils_extension.h" #include "extensions/openxr_eye_gaze_interaction.h" #include "extensions/openxr_fb_display_refresh_rate_extension.h" #include "extensions/openxr_hand_interaction_extension.h" @@ -133,6 +134,9 @@ void initialize_openxr_module(ModuleInitializationLevel p_level) { OpenXRAPI::register_extension_wrapper(memnew(OpenXRVisibilityMaskExtension)); // register gated extensions + if (int(GLOBAL_GET("xr/openxr/extensions/debug_utils")) > 0) { + OpenXRAPI::register_extension_wrapper(memnew(OpenXRDebugUtilsExtension)); + } if (GLOBAL_GET("xr/openxr/extensions/hand_tracking")) { OpenXRAPI::register_extension_wrapper(memnew(OpenXRHandTrackingExtension)); } diff --git a/modules/regex/doc_classes/RegEx.xml b/modules/regex/doc_classes/RegEx.xml index ab74fce3a9..e12dc43b6f 100644 --- a/modules/regex/doc_classes/RegEx.xml +++ b/modules/regex/doc_classes/RegEx.xml @@ -58,15 +58,17 @@ <method name="compile"> <return type="int" enum="Error" /> <param index="0" name="pattern" type="String" /> + <param index="1" name="show_error" type="bool" default="true" /> <description> - Compiles and assign the search pattern to use. Returns [constant OK] if the compilation is successful. If an error is encountered, details are printed to standard output and an error is returned. + Compiles and assign the search pattern to use. Returns [constant OK] if the compilation is successful. If compilation fails, returns [constant FAILED] and when [param show_error] is [code]true[/code], details are printed to standard output. </description> </method> <method name="create_from_string" qualifiers="static"> <return type="RegEx" /> <param index="0" name="pattern" type="String" /> + <param index="1" name="show_error" type="bool" default="true" /> <description> - Creates and compiles a new [RegEx] object. + Creates and compiles a new [RegEx] object. See also [method compile]. </description> </method> <method name="get_group_count" qualifiers="const"> diff --git a/modules/regex/regex.compat.inc b/modules/regex/regex.compat.inc new file mode 100644 index 0000000000..0c380655a4 --- /dev/null +++ b/modules/regex/regex.compat.inc @@ -0,0 +1,46 @@ +/**************************************************************************/ +/* regex.compat.inc */ +/**************************************************************************/ +/* 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. */ +/**************************************************************************/ + +#ifndef DISABLE_DEPRECATED + +Ref<RegEx> RegEx::_create_from_string_bind_compat_95212(const String &p_pattern) { + return create_from_string(p_pattern, true); +} + +Error RegEx::_compile_bind_compat_95212(const String &p_pattern) { + return compile(p_pattern, true); +} + +void RegEx::_bind_compatibility_methods() { + ClassDB::bind_compatibility_static_method("RegEx", D_METHOD("create_from_string", "pattern"), &RegEx::_create_from_string_bind_compat_95212); + ClassDB::bind_compatibility_method(D_METHOD("compile", "pattern"), &RegEx::_compile_bind_compat_95212); +} + +#endif diff --git a/modules/regex/regex.cpp b/modules/regex/regex.cpp index 9f34a6ca6a..85c0b9ecad 100644 --- a/modules/regex/regex.cpp +++ b/modules/regex/regex.cpp @@ -29,6 +29,7 @@ /**************************************************************************/ #include "regex.h" +#include "regex.compat.inc" #include "core/os/memory.h" @@ -161,10 +162,10 @@ void RegEx::_pattern_info(uint32_t what, void *where) const { pcre2_pattern_info_32((pcre2_code_32 *)code, what, where); } -Ref<RegEx> RegEx::create_from_string(const String &p_pattern) { +Ref<RegEx> RegEx::create_from_string(const String &p_pattern, bool p_show_error) { Ref<RegEx> ret; ret.instantiate(); - ret->compile(p_pattern); + ret->compile(p_pattern, p_show_error); return ret; } @@ -175,7 +176,7 @@ void RegEx::clear() { } } -Error RegEx::compile(const String &p_pattern) { +Error RegEx::compile(const String &p_pattern, bool p_show_error) { pattern = p_pattern; clear(); @@ -192,10 +193,12 @@ Error RegEx::compile(const String &p_pattern) { pcre2_compile_context_free_32(cctx); if (!code) { - PCRE2_UCHAR32 buf[256]; - pcre2_get_error_message_32(err, buf, 256); - String message = String::num(offset) + ": " + String((const char32_t *)buf); - ERR_PRINT(message.utf8()); + if (p_show_error) { + PCRE2_UCHAR32 buf[256]; + pcre2_get_error_message_32(err, buf, 256); + String message = String::num(offset) + ": " + String((const char32_t *)buf); + ERR_PRINT(message.utf8()); + } return FAILED; } return OK; @@ -395,10 +398,10 @@ RegEx::~RegEx() { } void RegEx::_bind_methods() { - ClassDB::bind_static_method("RegEx", D_METHOD("create_from_string", "pattern"), &RegEx::create_from_string); + ClassDB::bind_static_method("RegEx", D_METHOD("create_from_string", "pattern", "show_error"), &RegEx::create_from_string, DEFVAL(true)); ClassDB::bind_method(D_METHOD("clear"), &RegEx::clear); - ClassDB::bind_method(D_METHOD("compile", "pattern"), &RegEx::compile); + ClassDB::bind_method(D_METHOD("compile", "pattern", "show_error"), &RegEx::compile, DEFVAL(true)); ClassDB::bind_method(D_METHOD("search", "subject", "offset", "end"), &RegEx::search, DEFVAL(0), DEFVAL(-1)); ClassDB::bind_method(D_METHOD("search_all", "subject", "offset", "end"), &RegEx::search_all, DEFVAL(0), DEFVAL(-1)); ClassDB::bind_method(D_METHOD("sub", "subject", "replacement", "all", "offset", "end"), &RegEx::sub, DEFVAL(false), DEFVAL(0), DEFVAL(-1)); diff --git a/modules/regex/regex.h b/modules/regex/regex.h index 13476d69de..cb8b0459ad 100644 --- a/modules/regex/regex.h +++ b/modules/regex/regex.h @@ -81,11 +81,17 @@ class RegEx : public RefCounted { protected: static void _bind_methods(); +#ifndef DISABLE_DEPRECATED + static Ref<RegEx> _create_from_string_bind_compat_95212(const String &p_pattern); + Error _compile_bind_compat_95212(const String &p_pattern); + static void _bind_compatibility_methods(); +#endif + public: - static Ref<RegEx> create_from_string(const String &p_pattern); + static Ref<RegEx> create_from_string(const String &p_pattern, bool p_show_error = true); void clear(); - Error compile(const String &p_pattern); + Error compile(const String &p_pattern, bool p_show_error = true); Ref<RegExMatch> search(const String &p_subject, int p_offset = 0, int p_end = -1) const; TypedArray<RegExMatch> search_all(const String &p_subject, int p_offset = 0, int p_end = -1) const; diff --git a/modules/text_server_adv/text_server_adv.cpp b/modules/text_server_adv/text_server_adv.cpp index d0c22e9e4d..4bf09d3c84 100644 --- a/modules/text_server_adv/text_server_adv.cpp +++ b/modules/text_server_adv/text_server_adv.cpp @@ -136,11 +136,12 @@ hb_position_t TextServerAdvanced::_bmp_get_glyph_h_advance(hb_font_t *p_font, vo return 0; } - if (!bm_font->face->glyph_map.has(p_glyph)) { + HashMap<int32_t, FontGlyph>::Iterator E = bm_font->face->glyph_map.find(p_glyph); + if (!E) { return 0; } - return bm_font->face->glyph_map[p_glyph].advance.x * 64; + return E->value.advance.x * 64; } hb_position_t TextServerAdvanced::_bmp_get_glyph_v_advance(hb_font_t *p_font, void *p_font_data, hb_codepoint_t p_glyph, void *p_user_data) { @@ -150,11 +151,12 @@ hb_position_t TextServerAdvanced::_bmp_get_glyph_v_advance(hb_font_t *p_font, vo return 0; } - if (!bm_font->face->glyph_map.has(p_glyph)) { + HashMap<int32_t, FontGlyph>::Iterator E = bm_font->face->glyph_map.find(p_glyph); + if (!E) { return 0; } - return -bm_font->face->glyph_map[p_glyph].advance.y * 64; + return -E->value.advance.y * 64; } hb_position_t TextServerAdvanced::_bmp_get_glyph_h_kerning(hb_font_t *p_font, void *p_font_data, hb_codepoint_t p_left_glyph, hb_codepoint_t p_right_glyph, void *p_user_data) { @@ -178,11 +180,12 @@ hb_bool_t TextServerAdvanced::_bmp_get_glyph_v_origin(hb_font_t *p_font, void *p return false; } - if (!bm_font->face->glyph_map.has(p_glyph)) { + HashMap<int32_t, FontGlyph>::Iterator E = bm_font->face->glyph_map.find(p_glyph); + if (!E) { return false; } - *r_x = bm_font->face->glyph_map[p_glyph].advance.x * 32; + *r_x = E->value.advance.x * 32; *r_y = -bm_font->face->ascent * 64; return true; @@ -195,14 +198,15 @@ hb_bool_t TextServerAdvanced::_bmp_get_glyph_extents(hb_font_t *p_font, void *p_ return false; } - if (!bm_font->face->glyph_map.has(p_glyph)) { + HashMap<int32_t, FontGlyph>::Iterator E = bm_font->face->glyph_map.find(p_glyph); + if (!E) { return false; } r_extents->x_bearing = 0; r_extents->y_bearing = 0; - r_extents->width = bm_font->face->glyph_map[p_glyph].rect.size.x * 64; - r_extents->height = bm_font->face->glyph_map[p_glyph].rect.size.y * 64; + r_extents->width = E->value.rect.size.x * 64; + r_extents->height = E->value.rect.size.y * 64; return true; } @@ -1188,18 +1192,21 @@ _FORCE_INLINE_ TextServerAdvanced::FontGlyph TextServerAdvanced::rasterize_bitma /* Font Cache */ /*************************************************************************/ -_FORCE_INLINE_ bool TextServerAdvanced::_ensure_glyph(FontAdvanced *p_font_data, const Vector2i &p_size, int32_t p_glyph) const { - ERR_FAIL_COND_V(!_ensure_cache_for_size(p_font_data, p_size), false); +_FORCE_INLINE_ bool TextServerAdvanced::_ensure_glyph(FontAdvanced *p_font_data, const Vector2i &p_size, int32_t p_glyph, FontGlyph &r_glyph) const { + FontForSizeAdvanced *fd = nullptr; + ERR_FAIL_COND_V(!_ensure_cache_for_size(p_font_data, p_size, fd), false); int32_t glyph_index = p_glyph & 0xffffff; // Remove subpixel shifts. - FontForSizeAdvanced *fd = p_font_data->cache[p_size]; - if (fd->glyph_map.has(p_glyph)) { - return fd->glyph_map[p_glyph].found; + HashMap<int32_t, FontGlyph>::Iterator E = fd->glyph_map.find(p_glyph); + if (E) { + r_glyph = E->value; + return E->value.found; } if (glyph_index == 0) { // Non graphical or invalid glyph, do not render. - fd->glyph_map[p_glyph] = FontGlyph(); + E = fd->glyph_map.insert(p_glyph, FontGlyph()); + r_glyph = E->value; return true; } @@ -1235,7 +1242,8 @@ _FORCE_INLINE_ bool TextServerAdvanced::_ensure_glyph(FontAdvanced *p_font_data, int error = FT_Load_Glyph(fd->face, glyph_index, flags); if (error) { - fd->glyph_map[p_glyph] = FontGlyph(); + E = fd->glyph_map.insert(p_glyph, FontGlyph()); + r_glyph = E->value; return false; } @@ -1339,17 +1347,22 @@ _FORCE_INLINE_ bool TextServerAdvanced::_ensure_glyph(FontAdvanced *p_font_data, cleanup_stroker: FT_Stroker_Done(stroker); } - fd->glyph_map[p_glyph] = gl; + E = fd->glyph_map.insert(p_glyph, gl); + r_glyph = E->value; return gl.found; } #endif - fd->glyph_map[p_glyph] = FontGlyph(); + E = fd->glyph_map.insert(p_glyph, FontGlyph()); + r_glyph = E->value; return false; } -_FORCE_INLINE_ bool TextServerAdvanced::_ensure_cache_for_size(FontAdvanced *p_font_data, const Vector2i &p_size) const { +_FORCE_INLINE_ bool TextServerAdvanced::_ensure_cache_for_size(FontAdvanced *p_font_data, const Vector2i &p_size, FontForSizeAdvanced *&r_cache_for_size) const { ERR_FAIL_COND_V(p_size.x <= 0, false); - if (p_font_data->cache.has(p_size)) { + + HashMap<Vector2i, FontForSizeAdvanced *>::Iterator E = p_font_data->cache.find(p_size); + if (E) { + r_cache_for_size = E->value; return true; } @@ -1840,7 +1853,8 @@ _FORCE_INLINE_ bool TextServerAdvanced::_ensure_cache_for_size(FontAdvanced *p_f // Init bitmap font. fd->hb_handle = _bmp_font_create(fd, nullptr); } - p_font_data->cache[p_size] = fd; + p_font_data->cache.insert(p_size, fd); + r_cache_for_size = fd; return true; } @@ -1864,9 +1878,10 @@ hb_font_t *TextServerAdvanced::_font_get_hb_handle(const RID &p_font_rid, int64_ MutexLock lock(fd->mutex); Vector2i size = _get_size(fd, p_size); - ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size), nullptr); + FontForSizeAdvanced *ffsd = nullptr; + ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size, ffsd), nullptr); - return fd->cache[size]->hb_handle; + return ffsd->hb_handle; } RID TextServerAdvanced::_create_font() { @@ -1989,7 +2004,8 @@ void TextServerAdvanced::_font_set_style(const RID &p_font_rid, BitField<FontSty MutexLock lock(fd->mutex); Vector2i size = _get_size(fd, 16); - ERR_FAIL_COND(!_ensure_cache_for_size(fd, size)); + FontForSizeAdvanced *ffsd = nullptr; + ERR_FAIL_COND(!_ensure_cache_for_size(fd, size, ffsd)); fd->style_flags = p_style; } @@ -1999,7 +2015,8 @@ BitField<TextServer::FontStyle> TextServerAdvanced::_font_get_style(const RID &p MutexLock lock(fd->mutex); Vector2i size = _get_size(fd, 16); - ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size), 0); + FontForSizeAdvanced *ffsd = nullptr; + ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size, ffsd), 0); return fd->style_flags; } @@ -2009,7 +2026,8 @@ void TextServerAdvanced::_font_set_style_name(const RID &p_font_rid, const Strin MutexLock lock(fd->mutex); Vector2i size = _get_size(fd, 16); - ERR_FAIL_COND(!_ensure_cache_for_size(fd, size)); + FontForSizeAdvanced *ffsd = nullptr; + ERR_FAIL_COND(!_ensure_cache_for_size(fd, size, ffsd)); fd->style_name = p_name; } @@ -2019,7 +2037,8 @@ String TextServerAdvanced::_font_get_style_name(const RID &p_font_rid) const { MutexLock lock(fd->mutex); Vector2i size = _get_size(fd, 16); - ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size), String()); + FontForSizeAdvanced *ffsd = nullptr; + ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size, ffsd), String()); return fd->style_name; } @@ -2029,7 +2048,8 @@ void TextServerAdvanced::_font_set_weight(const RID &p_font_rid, int64_t p_weigh MutexLock lock(fd->mutex); Vector2i size = _get_size(fd, 16); - ERR_FAIL_COND(!_ensure_cache_for_size(fd, size)); + FontForSizeAdvanced *ffsd = nullptr; + ERR_FAIL_COND(!_ensure_cache_for_size(fd, size, ffsd)); fd->weight = CLAMP(p_weight, 100, 999); } @@ -2039,7 +2059,8 @@ int64_t TextServerAdvanced::_font_get_weight(const RID &p_font_rid) const { MutexLock lock(fd->mutex); Vector2i size = _get_size(fd, 16); - ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size), 400); + FontForSizeAdvanced *ffsd = nullptr; + ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size, ffsd), 400); return fd->weight; } @@ -2049,7 +2070,8 @@ void TextServerAdvanced::_font_set_stretch(const RID &p_font_rid, int64_t p_stre MutexLock lock(fd->mutex); Vector2i size = _get_size(fd, 16); - ERR_FAIL_COND(!_ensure_cache_for_size(fd, size)); + FontForSizeAdvanced *ffsd = nullptr; + ERR_FAIL_COND(!_ensure_cache_for_size(fd, size, ffsd)); fd->stretch = CLAMP(p_stretch, 50, 200); } @@ -2059,7 +2081,8 @@ int64_t TextServerAdvanced::_font_get_stretch(const RID &p_font_rid) const { MutexLock lock(fd->mutex); Vector2i size = _get_size(fd, 16); - ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size), 100); + FontForSizeAdvanced *ffsd = nullptr; + ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size, ffsd), 100); return fd->stretch; } @@ -2069,7 +2092,8 @@ void TextServerAdvanced::_font_set_name(const RID &p_font_rid, const String &p_n MutexLock lock(fd->mutex); Vector2i size = _get_size(fd, 16); - ERR_FAIL_COND(!_ensure_cache_for_size(fd, size)); + FontForSizeAdvanced *ffsd = nullptr; + ERR_FAIL_COND(!_ensure_cache_for_size(fd, size, ffsd)); fd->font_name = p_name; } @@ -2079,7 +2103,8 @@ String TextServerAdvanced::_font_get_name(const RID &p_font_rid) const { MutexLock lock(fd->mutex); Vector2i size = _get_size(fd, 16); - ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size), String()); + FontForSizeAdvanced *ffsd = nullptr; + ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size, ffsd), String()); return fd->font_name; } @@ -2089,9 +2114,10 @@ Dictionary TextServerAdvanced::_font_get_ot_name_strings(const RID &p_font_rid) MutexLock lock(fd->mutex); Vector2i size = _get_size(fd, 16); - ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size), Dictionary()); + FontForSizeAdvanced *ffsd = nullptr; + ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size, ffsd), Dictionary()); - hb_face_t *hb_face = hb_font_get_face(fd->cache[size]->hb_handle); + hb_face_t *hb_face = hb_font_get_face(ffsd->hb_handle); unsigned int num_entries = 0; const hb_ot_name_entry_t *names = hb_ot_name_list_names(hb_face, &num_entries); @@ -2599,8 +2625,9 @@ void TextServerAdvanced::_font_set_ascent(const RID &p_font_rid, int64_t p_size, MutexLock lock(fd->mutex); Vector2i size = _get_size(fd, p_size); - ERR_FAIL_COND(!_ensure_cache_for_size(fd, size)); - fd->cache[size]->ascent = p_ascent; + FontForSizeAdvanced *ffsd = nullptr; + ERR_FAIL_COND(!_ensure_cache_for_size(fd, size, ffsd)); + ffsd->ascent = p_ascent; } double TextServerAdvanced::_font_get_ascent(const RID &p_font_rid, int64_t p_size) const { @@ -2610,18 +2637,19 @@ double TextServerAdvanced::_font_get_ascent(const RID &p_font_rid, int64_t p_siz MutexLock lock(fd->mutex); Vector2i size = _get_size(fd, p_size); - ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size), 0.0); + FontForSizeAdvanced *ffsd = nullptr; + ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size, ffsd), 0.0); if (fd->msdf) { - return fd->cache[size]->ascent * (double)p_size / (double)fd->msdf_source_size; + return ffsd->ascent * (double)p_size / (double)fd->msdf_source_size; } else if (fd->fixed_size > 0 && fd->fixed_size_scale_mode != FIXED_SIZE_SCALE_DISABLE && size.x != p_size) { if (fd->fixed_size_scale_mode == FIXED_SIZE_SCALE_ENABLED) { - return fd->cache[size]->ascent * (double)p_size / (double)fd->fixed_size; + return ffsd->ascent * (double)p_size / (double)fd->fixed_size; } else { - return fd->cache[size]->ascent * Math::round((double)p_size / (double)fd->fixed_size); + return ffsd->ascent * Math::round((double)p_size / (double)fd->fixed_size); } } else { - return fd->cache[size]->ascent; + return ffsd->ascent; } } @@ -2631,8 +2659,9 @@ void TextServerAdvanced::_font_set_descent(const RID &p_font_rid, int64_t p_size Vector2i size = _get_size(fd, p_size); - ERR_FAIL_COND(!_ensure_cache_for_size(fd, size)); - fd->cache[size]->descent = p_descent; + FontForSizeAdvanced *ffsd = nullptr; + ERR_FAIL_COND(!_ensure_cache_for_size(fd, size, ffsd)); + ffsd->descent = p_descent; } double TextServerAdvanced::_font_get_descent(const RID &p_font_rid, int64_t p_size) const { @@ -2642,18 +2671,19 @@ double TextServerAdvanced::_font_get_descent(const RID &p_font_rid, int64_t p_si MutexLock lock(fd->mutex); Vector2i size = _get_size(fd, p_size); - ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size), 0.0); + FontForSizeAdvanced *ffsd = nullptr; + ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size, ffsd), 0.0); if (fd->msdf) { - return fd->cache[size]->descent * (double)p_size / (double)fd->msdf_source_size; + return ffsd->descent * (double)p_size / (double)fd->msdf_source_size; } else if (fd->fixed_size > 0 && fd->fixed_size_scale_mode != FIXED_SIZE_SCALE_DISABLE && size.x != p_size) { if (fd->fixed_size_scale_mode == FIXED_SIZE_SCALE_ENABLED) { - return fd->cache[size]->descent * (double)p_size / (double)fd->fixed_size; + return ffsd->descent * (double)p_size / (double)fd->fixed_size; } else { - return fd->cache[size]->descent * Math::round((double)p_size / (double)fd->fixed_size); + return ffsd->descent * Math::round((double)p_size / (double)fd->fixed_size); } } else { - return fd->cache[size]->descent; + return ffsd->descent; } } @@ -2664,8 +2694,9 @@ void TextServerAdvanced::_font_set_underline_position(const RID &p_font_rid, int MutexLock lock(fd->mutex); Vector2i size = _get_size(fd, p_size); - ERR_FAIL_COND(!_ensure_cache_for_size(fd, size)); - fd->cache[size]->underline_position = p_underline_position; + FontForSizeAdvanced *ffsd = nullptr; + ERR_FAIL_COND(!_ensure_cache_for_size(fd, size, ffsd)); + ffsd->underline_position = p_underline_position; } double TextServerAdvanced::_font_get_underline_position(const RID &p_font_rid, int64_t p_size) const { @@ -2675,18 +2706,19 @@ double TextServerAdvanced::_font_get_underline_position(const RID &p_font_rid, i MutexLock lock(fd->mutex); Vector2i size = _get_size(fd, p_size); - ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size), 0.0); + FontForSizeAdvanced *ffsd = nullptr; + ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size, ffsd), 0.0); if (fd->msdf) { - return fd->cache[size]->underline_position * (double)p_size / (double)fd->msdf_source_size; + return ffsd->underline_position * (double)p_size / (double)fd->msdf_source_size; } else if (fd->fixed_size > 0 && fd->fixed_size_scale_mode != FIXED_SIZE_SCALE_DISABLE && size.x != p_size) { if (fd->fixed_size_scale_mode == FIXED_SIZE_SCALE_ENABLED) { - return fd->cache[size]->underline_position * (double)p_size / (double)fd->fixed_size; + return ffsd->underline_position * (double)p_size / (double)fd->fixed_size; } else { - return fd->cache[size]->underline_position * Math::round((double)p_size / (double)fd->fixed_size); + return ffsd->underline_position * Math::round((double)p_size / (double)fd->fixed_size); } } else { - return fd->cache[size]->underline_position; + return ffsd->underline_position; } } @@ -2697,8 +2729,9 @@ void TextServerAdvanced::_font_set_underline_thickness(const RID &p_font_rid, in MutexLock lock(fd->mutex); Vector2i size = _get_size(fd, p_size); - ERR_FAIL_COND(!_ensure_cache_for_size(fd, size)); - fd->cache[size]->underline_thickness = p_underline_thickness; + FontForSizeAdvanced *ffsd = nullptr; + ERR_FAIL_COND(!_ensure_cache_for_size(fd, size, ffsd)); + ffsd->underline_thickness = p_underline_thickness; } double TextServerAdvanced::_font_get_underline_thickness(const RID &p_font_rid, int64_t p_size) const { @@ -2708,18 +2741,19 @@ double TextServerAdvanced::_font_get_underline_thickness(const RID &p_font_rid, MutexLock lock(fd->mutex); Vector2i size = _get_size(fd, p_size); - ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size), 0.0); + FontForSizeAdvanced *ffsd = nullptr; + ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size, ffsd), 0.0); if (fd->msdf) { - return fd->cache[size]->underline_thickness * (double)p_size / (double)fd->msdf_source_size; + return ffsd->underline_thickness * (double)p_size / (double)fd->msdf_source_size; } else if (fd->fixed_size > 0 && fd->fixed_size_scale_mode != FIXED_SIZE_SCALE_DISABLE && size.x != p_size) { if (fd->fixed_size_scale_mode == FIXED_SIZE_SCALE_ENABLED) { - return fd->cache[size]->underline_thickness * (double)p_size / (double)fd->fixed_size; + return ffsd->underline_thickness * (double)p_size / (double)fd->fixed_size; } else { - return fd->cache[size]->underline_thickness * Math::round((double)p_size / (double)fd->fixed_size); + return ffsd->underline_thickness * Math::round((double)p_size / (double)fd->fixed_size); } } else { - return fd->cache[size]->underline_thickness; + return ffsd->underline_thickness; } } @@ -2730,13 +2764,15 @@ void TextServerAdvanced::_font_set_scale(const RID &p_font_rid, int64_t p_size, MutexLock lock(fd->mutex); Vector2i size = _get_size(fd, p_size); - ERR_FAIL_COND(!_ensure_cache_for_size(fd, size)); + FontForSizeAdvanced *ffsd = nullptr; + ERR_FAIL_COND(!_ensure_cache_for_size(fd, size, ffsd)); + #ifdef MODULE_FREETYPE_ENABLED - if (fd->cache[size]->face) { + if (ffsd->face) { return; // Do not override scale for dynamic fonts, it's calculated automatically. } #endif - fd->cache[size]->scale = p_scale; + ffsd->scale = p_scale; } double TextServerAdvanced::_font_get_scale(const RID &p_font_rid, int64_t p_size) const { @@ -2746,18 +2782,19 @@ double TextServerAdvanced::_font_get_scale(const RID &p_font_rid, int64_t p_size MutexLock lock(fd->mutex); Vector2i size = _get_size(fd, p_size); - ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size), 0.0); + FontForSizeAdvanced *ffsd = nullptr; + ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size, ffsd), 0.0); if (fd->msdf) { - return fd->cache[size]->scale * (double)p_size / (double)fd->msdf_source_size; + return ffsd->scale * (double)p_size / (double)fd->msdf_source_size; } else if (fd->fixed_size > 0 && fd->fixed_size_scale_mode != FIXED_SIZE_SCALE_DISABLE && size.x != p_size) { if (fd->fixed_size_scale_mode == FIXED_SIZE_SCALE_ENABLED) { - return fd->cache[size]->scale * (double)p_size / (double)fd->fixed_size; + return ffsd->scale * (double)p_size / (double)fd->fixed_size; } else { - return fd->cache[size]->scale * Math::round((double)p_size / (double)fd->fixed_size); + return ffsd->scale * Math::round((double)p_size / (double)fd->fixed_size); } } else { - return fd->cache[size]->scale / fd->cache[size]->oversampling; + return ffsd->scale / ffsd->oversampling; } } @@ -2768,9 +2805,10 @@ int64_t TextServerAdvanced::_font_get_texture_count(const RID &p_font_rid, const MutexLock lock(fd->mutex); Vector2i size = _get_size_outline(fd, p_size); - ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size), 0); + FontForSizeAdvanced *ffsd = nullptr; + ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size, ffsd), 0); - return fd->cache[size]->textures.size(); + return ffsd->textures.size(); } void TextServerAdvanced::_font_clear_textures(const RID &p_font_rid, const Vector2i &p_size) { @@ -2779,8 +2817,9 @@ void TextServerAdvanced::_font_clear_textures(const RID &p_font_rid, const Vecto MutexLock lock(fd->mutex); Vector2i size = _get_size_outline(fd, p_size); - ERR_FAIL_COND(!_ensure_cache_for_size(fd, size)); - fd->cache[size]->textures.clear(); + FontForSizeAdvanced *ffsd = nullptr; + ERR_FAIL_COND(!_ensure_cache_for_size(fd, size, ffsd)); + ffsd->textures.clear(); } void TextServerAdvanced::_font_remove_texture(const RID &p_font_rid, const Vector2i &p_size, int64_t p_texture_index) { @@ -2789,10 +2828,11 @@ void TextServerAdvanced::_font_remove_texture(const RID &p_font_rid, const Vecto MutexLock lock(fd->mutex); Vector2i size = _get_size_outline(fd, p_size); - ERR_FAIL_COND(!_ensure_cache_for_size(fd, size)); - ERR_FAIL_INDEX(p_texture_index, fd->cache[size]->textures.size()); + FontForSizeAdvanced *ffsd = nullptr; + ERR_FAIL_COND(!_ensure_cache_for_size(fd, size, ffsd)); + ERR_FAIL_INDEX(p_texture_index, ffsd->textures.size()); - fd->cache[size]->textures.remove_at(p_texture_index); + ffsd->textures.remove_at(p_texture_index); } void TextServerAdvanced::_font_set_texture_image(const RID &p_font_rid, const Vector2i &p_size, int64_t p_texture_index, const Ref<Image> &p_image) { @@ -2802,13 +2842,14 @@ void TextServerAdvanced::_font_set_texture_image(const RID &p_font_rid, const Ve MutexLock lock(fd->mutex); Vector2i size = _get_size_outline(fd, p_size); - ERR_FAIL_COND(!_ensure_cache_for_size(fd, size)); + FontForSizeAdvanced *ffsd = nullptr; + ERR_FAIL_COND(!_ensure_cache_for_size(fd, size, ffsd)); ERR_FAIL_COND(p_texture_index < 0); - if (p_texture_index >= fd->cache[size]->textures.size()) { - fd->cache[size]->textures.resize(p_texture_index + 1); + if (p_texture_index >= ffsd->textures.size()) { + ffsd->textures.resize(p_texture_index + 1); } - ShelfPackTexture &tex = fd->cache[size]->textures.write[p_texture_index]; + ShelfPackTexture &tex = ffsd->textures.write[p_texture_index]; tex.image = p_image; tex.texture_w = p_image->get_width(); @@ -2829,10 +2870,11 @@ Ref<Image> TextServerAdvanced::_font_get_texture_image(const RID &p_font_rid, co MutexLock lock(fd->mutex); Vector2i size = _get_size_outline(fd, p_size); - ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size), Ref<Image>()); - ERR_FAIL_INDEX_V(p_texture_index, fd->cache[size]->textures.size(), Ref<Image>()); + FontForSizeAdvanced *ffsd = nullptr; + ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size, ffsd), Ref<Image>()); + ERR_FAIL_INDEX_V(p_texture_index, ffsd->textures.size(), Ref<Image>()); - const ShelfPackTexture &tex = fd->cache[size]->textures[p_texture_index]; + const ShelfPackTexture &tex = ffsd->textures[p_texture_index]; return tex.image; } @@ -2843,13 +2885,14 @@ void TextServerAdvanced::_font_set_texture_offsets(const RID &p_font_rid, const MutexLock lock(fd->mutex); Vector2i size = _get_size_outline(fd, p_size); - ERR_FAIL_COND(!_ensure_cache_for_size(fd, size)); + FontForSizeAdvanced *ffsd = nullptr; + ERR_FAIL_COND(!_ensure_cache_for_size(fd, size, ffsd)); ERR_FAIL_COND(p_texture_index < 0); - if (p_texture_index >= fd->cache[size]->textures.size()) { - fd->cache[size]->textures.resize(p_texture_index + 1); + if (p_texture_index >= ffsd->textures.size()) { + ffsd->textures.resize(p_texture_index + 1); } - ShelfPackTexture &tex = fd->cache[size]->textures.write[p_texture_index]; + ShelfPackTexture &tex = ffsd->textures.write[p_texture_index]; tex.shelves.clear(); for (int32_t i = 0; i < p_offsets.size(); i += 4) { tex.shelves.push_back(Shelf(p_offsets[i], p_offsets[i + 1], p_offsets[i + 2], p_offsets[i + 3])); @@ -2862,10 +2905,11 @@ PackedInt32Array TextServerAdvanced::_font_get_texture_offsets(const RID &p_font MutexLock lock(fd->mutex); Vector2i size = _get_size_outline(fd, p_size); - ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size), PackedInt32Array()); - ERR_FAIL_INDEX_V(p_texture_index, fd->cache[size]->textures.size(), PackedInt32Array()); + FontForSizeAdvanced *ffsd = nullptr; + ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size, ffsd), PackedInt32Array()); + ERR_FAIL_INDEX_V(p_texture_index, ffsd->textures.size(), PackedInt32Array()); - const ShelfPackTexture &tex = fd->cache[size]->textures[p_texture_index]; + const ShelfPackTexture &tex = ffsd->textures[p_texture_index]; PackedInt32Array ret; ret.resize(tex.shelves.size() * 4); @@ -2887,10 +2931,11 @@ PackedInt32Array TextServerAdvanced::_font_get_glyph_list(const RID &p_font_rid, MutexLock lock(fd->mutex); Vector2i size = _get_size_outline(fd, p_size); - ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size), PackedInt32Array()); + FontForSizeAdvanced *ffsd = nullptr; + ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size, ffsd), PackedInt32Array()); PackedInt32Array ret; - const HashMap<int32_t, FontGlyph> &gl = fd->cache[size]->glyph_map; + const HashMap<int32_t, FontGlyph> &gl = ffsd->glyph_map; for (const KeyValue<int32_t, FontGlyph> &E : gl) { ret.push_back(E.key); } @@ -2903,9 +2948,10 @@ void TextServerAdvanced::_font_clear_glyphs(const RID &p_font_rid, const Vector2 MutexLock lock(fd->mutex); Vector2i size = _get_size_outline(fd, p_size); - ERR_FAIL_COND(!_ensure_cache_for_size(fd, size)); + FontForSizeAdvanced *ffsd = nullptr; + ERR_FAIL_COND(!_ensure_cache_for_size(fd, size, ffsd)); - fd->cache[size]->glyph_map.clear(); + ffsd->glyph_map.clear(); } void TextServerAdvanced::_font_remove_glyph(const RID &p_font_rid, const Vector2i &p_size, int64_t p_glyph) { @@ -2914,9 +2960,10 @@ void TextServerAdvanced::_font_remove_glyph(const RID &p_font_rid, const Vector2 MutexLock lock(fd->mutex); Vector2i size = _get_size_outline(fd, p_size); - ERR_FAIL_COND(!_ensure_cache_for_size(fd, size)); + FontForSizeAdvanced *ffsd = nullptr; + ERR_FAIL_COND(!_ensure_cache_for_size(fd, size, ffsd)); - fd->cache[size]->glyph_map.erase(p_glyph); + ffsd->glyph_map.erase(p_glyph); } double TextServerAdvanced::_get_extra_advance(RID p_font_rid, int p_font_size) const { @@ -2940,22 +2987,22 @@ Vector2 TextServerAdvanced::_font_get_glyph_advance(const RID &p_font_rid, int64 MutexLock lock(fd->mutex); Vector2i size = _get_size(fd, p_size); - ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size), Vector2()); + FontForSizeAdvanced *ffsd = nullptr; + ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size, ffsd), Vector2()); int mod = 0; if (fd->antialiasing == FONT_ANTIALIASING_LCD) { - TextServer::FontLCDSubpixelLayout layout = (TextServer::FontLCDSubpixelLayout)(int)GLOBAL_GET("gui/theme/lcd_subpixel_layout"); + TextServer::FontLCDSubpixelLayout layout = lcd_subpixel_layout.get(); if (layout != FONT_LCD_SUBPIXEL_LAYOUT_NONE) { mod = (layout << 24); } } - if (!_ensure_glyph(fd, size, p_glyph | mod)) { + FontGlyph fgl; + if (!_ensure_glyph(fd, size, p_glyph | mod, fgl)) { return Vector2(); // Invalid or non graphicl glyph, do not display errors. } - const HashMap<int32_t, FontGlyph> &gl = fd->cache[size]->glyph_map; - Vector2 ea; if (fd->embolden != 0.0) { ea.x = fd->embolden * double(size.x) / 64.0; @@ -2963,17 +3010,17 @@ Vector2 TextServerAdvanced::_font_get_glyph_advance(const RID &p_font_rid, int64 double scale = _font_get_scale(p_font_rid, p_size); if (fd->msdf) { - return (gl[p_glyph | mod].advance + ea) * (double)p_size / (double)fd->msdf_source_size; + return (fgl.advance + ea) * (double)p_size / (double)fd->msdf_source_size; } else if (fd->fixed_size > 0 && fd->fixed_size_scale_mode != FIXED_SIZE_SCALE_DISABLE && size.x != p_size) { if (fd->fixed_size_scale_mode == FIXED_SIZE_SCALE_ENABLED) { - return (gl[p_glyph | mod].advance + ea) * (double)p_size / (double)fd->fixed_size; + return (fgl.advance + ea) * (double)p_size / (double)fd->fixed_size; } else { - return (gl[p_glyph | mod].advance + ea) * Math::round((double)p_size / (double)fd->fixed_size); + return (fgl.advance + ea) * Math::round((double)p_size / (double)fd->fixed_size); } } else if ((scale == 1.0) && ((fd->subpixel_positioning == SUBPIXEL_POSITIONING_DISABLED) || (fd->subpixel_positioning == SUBPIXEL_POSITIONING_AUTO && size.x > SUBPIXEL_POSITIONING_ONE_HALF_MAX_SIZE))) { - return (gl[p_glyph | mod].advance + ea).round(); + return (fgl.advance + ea).round(); } else { - return gl[p_glyph | mod].advance + ea; + return fgl.advance + ea; } } @@ -2984,12 +3031,13 @@ void TextServerAdvanced::_font_set_glyph_advance(const RID &p_font_rid, int64_t MutexLock lock(fd->mutex); Vector2i size = _get_size(fd, p_size); - ERR_FAIL_COND(!_ensure_cache_for_size(fd, size)); + FontForSizeAdvanced *ffsd = nullptr; + ERR_FAIL_COND(!_ensure_cache_for_size(fd, size, ffsd)); - HashMap<int32_t, FontGlyph> &gl = fd->cache[size]->glyph_map; + FontGlyph &fgl = ffsd->glyph_map[p_glyph]; - gl[p_glyph].advance = p_advance; - gl[p_glyph].found = true; + fgl.advance = p_advance; + fgl.found = true; } Vector2 TextServerAdvanced::_font_get_glyph_offset(const RID &p_font_rid, const Vector2i &p_size, int64_t p_glyph) const { @@ -2999,32 +3047,32 @@ Vector2 TextServerAdvanced::_font_get_glyph_offset(const RID &p_font_rid, const MutexLock lock(fd->mutex); Vector2i size = _get_size_outline(fd, p_size); - ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size), Vector2()); + FontForSizeAdvanced *ffsd = nullptr; + ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size, ffsd), Vector2()); int mod = 0; if (fd->antialiasing == FONT_ANTIALIASING_LCD) { - TextServer::FontLCDSubpixelLayout layout = (TextServer::FontLCDSubpixelLayout)(int)GLOBAL_GET("gui/theme/lcd_subpixel_layout"); + TextServer::FontLCDSubpixelLayout layout = lcd_subpixel_layout.get(); if (layout != FONT_LCD_SUBPIXEL_LAYOUT_NONE) { mod = (layout << 24); } } - if (!_ensure_glyph(fd, size, p_glyph | mod)) { + FontGlyph fgl; + if (!_ensure_glyph(fd, size, p_glyph | mod, fgl)) { return Vector2(); // Invalid or non graphicl glyph, do not display errors. } - const HashMap<int32_t, FontGlyph> &gl = fd->cache[size]->glyph_map; - if (fd->msdf) { - return gl[p_glyph | mod].rect.position * (double)p_size.x / (double)fd->msdf_source_size; + return fgl.rect.position * (double)p_size.x / (double)fd->msdf_source_size; } else if (fd->fixed_size > 0 && fd->fixed_size_scale_mode != FIXED_SIZE_SCALE_DISABLE && size.x != p_size.x) { if (fd->fixed_size_scale_mode == FIXED_SIZE_SCALE_ENABLED) { - return gl[p_glyph | mod].rect.position * (double)p_size.x / (double)fd->fixed_size; + return fgl.rect.position * (double)p_size.x / (double)fd->fixed_size; } else { - return gl[p_glyph | mod].rect.position * Math::round((double)p_size.x / (double)fd->fixed_size); + return fgl.rect.position * Math::round((double)p_size.x / (double)fd->fixed_size); } } else { - return gl[p_glyph | mod].rect.position; + return fgl.rect.position; } } @@ -3035,12 +3083,13 @@ void TextServerAdvanced::_font_set_glyph_offset(const RID &p_font_rid, const Vec MutexLock lock(fd->mutex); Vector2i size = _get_size_outline(fd, p_size); - ERR_FAIL_COND(!_ensure_cache_for_size(fd, size)); + FontForSizeAdvanced *ffsd = nullptr; + ERR_FAIL_COND(!_ensure_cache_for_size(fd, size, ffsd)); - HashMap<int32_t, FontGlyph> &gl = fd->cache[size]->glyph_map; + FontGlyph &fgl = ffsd->glyph_map[p_glyph]; - gl[p_glyph].rect.position = p_offset; - gl[p_glyph].found = true; + fgl.rect.position = p_offset; + fgl.found = true; } Vector2 TextServerAdvanced::_font_get_glyph_size(const RID &p_font_rid, const Vector2i &p_size, int64_t p_glyph) const { @@ -3050,32 +3099,32 @@ Vector2 TextServerAdvanced::_font_get_glyph_size(const RID &p_font_rid, const Ve MutexLock lock(fd->mutex); Vector2i size = _get_size_outline(fd, p_size); - ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size), Vector2()); + FontForSizeAdvanced *ffsd = nullptr; + ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size, ffsd), Vector2()); int mod = 0; if (fd->antialiasing == FONT_ANTIALIASING_LCD) { - TextServer::FontLCDSubpixelLayout layout = (TextServer::FontLCDSubpixelLayout)(int)GLOBAL_GET("gui/theme/lcd_subpixel_layout"); + TextServer::FontLCDSubpixelLayout layout = lcd_subpixel_layout.get(); if (layout != FONT_LCD_SUBPIXEL_LAYOUT_NONE) { mod = (layout << 24); } } - if (!_ensure_glyph(fd, size, p_glyph | mod)) { + FontGlyph fgl; + if (!_ensure_glyph(fd, size, p_glyph | mod, fgl)) { return Vector2(); // Invalid or non graphicl glyph, do not display errors. } - const HashMap<int32_t, FontGlyph> &gl = fd->cache[size]->glyph_map; - if (fd->msdf) { - return gl[p_glyph | mod].rect.size * (double)p_size.x / (double)fd->msdf_source_size; + return fgl.rect.size * (double)p_size.x / (double)fd->msdf_source_size; } else if (fd->fixed_size > 0 && fd->fixed_size_scale_mode != FIXED_SIZE_SCALE_DISABLE && size.x != p_size.x) { if (fd->fixed_size_scale_mode == FIXED_SIZE_SCALE_ENABLED) { - return gl[p_glyph | mod].rect.size * (double)p_size.x / (double)fd->fixed_size; + return fgl.rect.size * (double)p_size.x / (double)fd->fixed_size; } else { - return gl[p_glyph | mod].rect.size * Math::round((double)p_size.x / (double)fd->fixed_size); + return fgl.rect.size * Math::round((double)p_size.x / (double)fd->fixed_size); } } else { - return gl[p_glyph | mod].rect.size; + return fgl.rect.size; } } @@ -3086,12 +3135,13 @@ void TextServerAdvanced::_font_set_glyph_size(const RID &p_font_rid, const Vecto MutexLock lock(fd->mutex); Vector2i size = _get_size_outline(fd, p_size); - ERR_FAIL_COND(!_ensure_cache_for_size(fd, size)); + FontForSizeAdvanced *ffsd = nullptr; + ERR_FAIL_COND(!_ensure_cache_for_size(fd, size, ffsd)); - HashMap<int32_t, FontGlyph> &gl = fd->cache[size]->glyph_map; + FontGlyph &fgl = ffsd->glyph_map[p_glyph]; - gl[p_glyph].rect.size = p_gl_size; - gl[p_glyph].found = true; + fgl.rect.size = p_gl_size; + fgl.found = true; } Rect2 TextServerAdvanced::_font_get_glyph_uv_rect(const RID &p_font_rid, const Vector2i &p_size, int64_t p_glyph) const { @@ -3101,22 +3151,23 @@ Rect2 TextServerAdvanced::_font_get_glyph_uv_rect(const RID &p_font_rid, const V MutexLock lock(fd->mutex); Vector2i size = _get_size_outline(fd, p_size); - ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size), Rect2()); + FontForSizeAdvanced *ffsd = nullptr; + ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size, ffsd), Rect2()); int mod = 0; if (fd->antialiasing == FONT_ANTIALIASING_LCD) { - TextServer::FontLCDSubpixelLayout layout = (TextServer::FontLCDSubpixelLayout)(int)GLOBAL_GET("gui/theme/lcd_subpixel_layout"); + TextServer::FontLCDSubpixelLayout layout = lcd_subpixel_layout.get(); if (layout != FONT_LCD_SUBPIXEL_LAYOUT_NONE) { mod = (layout << 24); } } - if (!_ensure_glyph(fd, size, p_glyph | mod)) { + FontGlyph fgl; + if (!_ensure_glyph(fd, size, p_glyph | mod, fgl)) { return Rect2(); // Invalid or non graphicl glyph, do not display errors. } - const HashMap<int32_t, FontGlyph> &gl = fd->cache[size]->glyph_map; - return gl[p_glyph | mod].uv_rect; + return fgl.uv_rect; } void TextServerAdvanced::_font_set_glyph_uv_rect(const RID &p_font_rid, const Vector2i &p_size, int64_t p_glyph, const Rect2 &p_uv_rect) { @@ -3126,12 +3177,13 @@ void TextServerAdvanced::_font_set_glyph_uv_rect(const RID &p_font_rid, const Ve MutexLock lock(fd->mutex); Vector2i size = _get_size_outline(fd, p_size); - ERR_FAIL_COND(!_ensure_cache_for_size(fd, size)); + FontForSizeAdvanced *ffsd = nullptr; + ERR_FAIL_COND(!_ensure_cache_for_size(fd, size, ffsd)); - HashMap<int32_t, FontGlyph> &gl = fd->cache[size]->glyph_map; + FontGlyph &fgl = ffsd->glyph_map[p_glyph]; - gl[p_glyph].uv_rect = p_uv_rect; - gl[p_glyph].found = true; + fgl.uv_rect = p_uv_rect; + fgl.found = true; } int64_t TextServerAdvanced::_font_get_glyph_texture_idx(const RID &p_font_rid, const Vector2i &p_size, int64_t p_glyph) const { @@ -3141,22 +3193,23 @@ int64_t TextServerAdvanced::_font_get_glyph_texture_idx(const RID &p_font_rid, c MutexLock lock(fd->mutex); Vector2i size = _get_size_outline(fd, p_size); - ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size), -1); + FontForSizeAdvanced *ffsd = nullptr; + ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size, ffsd), -1); int mod = 0; if (fd->antialiasing == FONT_ANTIALIASING_LCD) { - TextServer::FontLCDSubpixelLayout layout = (TextServer::FontLCDSubpixelLayout)(int)GLOBAL_GET("gui/theme/lcd_subpixel_layout"); + TextServer::FontLCDSubpixelLayout layout = lcd_subpixel_layout.get(); if (layout != FONT_LCD_SUBPIXEL_LAYOUT_NONE) { mod = (layout << 24); } } - if (!_ensure_glyph(fd, size, p_glyph | mod)) { + FontGlyph fgl; + if (!_ensure_glyph(fd, size, p_glyph | mod, fgl)) { return -1; // Invalid or non graphicl glyph, do not display errors. } - const HashMap<int32_t, FontGlyph> &gl = fd->cache[size]->glyph_map; - return gl[p_glyph | mod].texture_idx; + return fgl.texture_idx; } void TextServerAdvanced::_font_set_glyph_texture_idx(const RID &p_font_rid, const Vector2i &p_size, int64_t p_glyph, int64_t p_texture_idx) { @@ -3166,12 +3219,13 @@ void TextServerAdvanced::_font_set_glyph_texture_idx(const RID &p_font_rid, cons MutexLock lock(fd->mutex); Vector2i size = _get_size_outline(fd, p_size); - ERR_FAIL_COND(!_ensure_cache_for_size(fd, size)); + FontForSizeAdvanced *ffsd = nullptr; + ERR_FAIL_COND(!_ensure_cache_for_size(fd, size, ffsd)); - HashMap<int32_t, FontGlyph> &gl = fd->cache[size]->glyph_map; + FontGlyph &fgl = ffsd->glyph_map[p_glyph]; - gl[p_glyph].texture_idx = p_texture_idx; - gl[p_glyph].found = true; + fgl.texture_idx = p_texture_idx; + fgl.found = true; } RID TextServerAdvanced::_font_get_glyph_texture_rid(const RID &p_font_rid, const Vector2i &p_size, int64_t p_glyph) const { @@ -3181,27 +3235,28 @@ RID TextServerAdvanced::_font_get_glyph_texture_rid(const RID &p_font_rid, const MutexLock lock(fd->mutex); Vector2i size = _get_size_outline(fd, p_size); - ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size), RID()); + FontForSizeAdvanced *ffsd = nullptr; + ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size, ffsd), RID()); int mod = 0; if (fd->antialiasing == FONT_ANTIALIASING_LCD) { - TextServer::FontLCDSubpixelLayout layout = (TextServer::FontLCDSubpixelLayout)(int)GLOBAL_GET("gui/theme/lcd_subpixel_layout"); + TextServer::FontLCDSubpixelLayout layout = lcd_subpixel_layout.get(); if (layout != FONT_LCD_SUBPIXEL_LAYOUT_NONE) { mod = (layout << 24); } } - if (!_ensure_glyph(fd, size, p_glyph | mod)) { + FontGlyph fgl; + if (!_ensure_glyph(fd, size, p_glyph | mod, fgl)) { return RID(); // Invalid or non graphicl glyph, do not display errors. } - const HashMap<int32_t, FontGlyph> &gl = fd->cache[size]->glyph_map; - ERR_FAIL_COND_V(gl[p_glyph | mod].texture_idx < -1 || gl[p_glyph | mod].texture_idx >= fd->cache[size]->textures.size(), RID()); + ERR_FAIL_COND_V(fgl.texture_idx < -1 || fgl.texture_idx >= ffsd->textures.size(), RID()); if (RenderingServer::get_singleton() != nullptr) { - if (gl[p_glyph | mod].texture_idx != -1) { - if (fd->cache[size]->textures[gl[p_glyph | mod].texture_idx].dirty) { - ShelfPackTexture &tex = fd->cache[size]->textures.write[gl[p_glyph | mod].texture_idx]; + if (fgl.texture_idx != -1) { + if (ffsd->textures[fgl.texture_idx].dirty) { + ShelfPackTexture &tex = ffsd->textures.write[fgl.texture_idx]; Ref<Image> img = tex.image; if (fd->mipmaps && !img->has_mipmaps()) { img = tex.image->duplicate(); @@ -3214,7 +3269,7 @@ RID TextServerAdvanced::_font_get_glyph_texture_rid(const RID &p_font_rid, const } tex.dirty = false; } - return fd->cache[size]->textures[gl[p_glyph | mod].texture_idx].texture->get_rid(); + return ffsd->textures[fgl.texture_idx].texture->get_rid(); } } @@ -3228,27 +3283,28 @@ Size2 TextServerAdvanced::_font_get_glyph_texture_size(const RID &p_font_rid, co MutexLock lock(fd->mutex); Vector2i size = _get_size_outline(fd, p_size); - ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size), Size2()); + FontForSizeAdvanced *ffsd = nullptr; + ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size, ffsd), Size2()); int mod = 0; if (fd->antialiasing == FONT_ANTIALIASING_LCD) { - TextServer::FontLCDSubpixelLayout layout = (TextServer::FontLCDSubpixelLayout)(int)GLOBAL_GET("gui/theme/lcd_subpixel_layout"); + TextServer::FontLCDSubpixelLayout layout = lcd_subpixel_layout.get(); if (layout != FONT_LCD_SUBPIXEL_LAYOUT_NONE) { mod = (layout << 24); } } - if (!_ensure_glyph(fd, size, p_glyph | mod)) { + FontGlyph fgl; + if (!_ensure_glyph(fd, size, p_glyph | mod, fgl)) { return Size2(); // Invalid or non graphicl glyph, do not display errors. } - const HashMap<int32_t, FontGlyph> &gl = fd->cache[size]->glyph_map; - ERR_FAIL_COND_V(gl[p_glyph | mod].texture_idx < -1 || gl[p_glyph | mod].texture_idx >= fd->cache[size]->textures.size(), Size2()); + ERR_FAIL_COND_V(fgl.texture_idx < -1 || fgl.texture_idx >= ffsd->textures.size(), Size2()); if (RenderingServer::get_singleton() != nullptr) { - if (gl[p_glyph | mod].texture_idx != -1) { - if (fd->cache[size]->textures[gl[p_glyph | mod].texture_idx].dirty) { - ShelfPackTexture &tex = fd->cache[size]->textures.write[gl[p_glyph | mod].texture_idx]; + if (fgl.texture_idx != -1) { + if (ffsd->textures[fgl.texture_idx].dirty) { + ShelfPackTexture &tex = ffsd->textures.write[fgl.texture_idx]; Ref<Image> img = tex.image; if (fd->mipmaps && !img->has_mipmaps()) { img = tex.image->duplicate(); @@ -3261,7 +3317,7 @@ Size2 TextServerAdvanced::_font_get_glyph_texture_size(const RID &p_font_rid, co } tex.dirty = false; } - return fd->cache[size]->textures[gl[p_glyph | mod].texture_idx].texture->get_size(); + return ffsd->textures[fgl.texture_idx].texture->get_size(); } } @@ -3275,7 +3331,8 @@ Dictionary TextServerAdvanced::_font_get_glyph_contours(const RID &p_font_rid, i MutexLock lock(fd->mutex); Vector2i size = _get_size(fd, p_size); - ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size), Dictionary()); + FontForSizeAdvanced *ffsd = nullptr; + ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size, ffsd), Dictionary()); #ifdef MODULE_FREETYPE_ENABLED PackedVector3Array points; @@ -3283,20 +3340,20 @@ Dictionary TextServerAdvanced::_font_get_glyph_contours(const RID &p_font_rid, i int32_t index = p_index & 0xffffff; // Remove subpixel shifts. - int error = FT_Load_Glyph(fd->cache[size]->face, index, FT_LOAD_NO_BITMAP | (fd->force_autohinter ? FT_LOAD_FORCE_AUTOHINT : 0)); + int error = FT_Load_Glyph(ffsd->face, index, FT_LOAD_NO_BITMAP | (fd->force_autohinter ? FT_LOAD_FORCE_AUTOHINT : 0)); ERR_FAIL_COND_V(error, Dictionary()); if (fd->embolden != 0.f) { FT_Pos strength = fd->embolden * p_size * 4; // 26.6 fractional units (1 / 64). - FT_Outline_Embolden(&fd->cache[size]->face->glyph->outline, strength); + FT_Outline_Embolden(&ffsd->face->glyph->outline, strength); } if (fd->transform != Transform2D()) { FT_Matrix mat = { FT_Fixed(fd->transform[0][0] * 65536), FT_Fixed(fd->transform[0][1] * 65536), FT_Fixed(fd->transform[1][0] * 65536), FT_Fixed(fd->transform[1][1] * 65536) }; // 16.16 fractional units (1 / 65536). - FT_Outline_Transform(&fd->cache[size]->face->glyph->outline, &mat); + FT_Outline_Transform(&ffsd->face->glyph->outline, &mat); } - double scale = (1.0 / 64.0) / fd->cache[size]->oversampling * fd->cache[size]->scale; + double scale = (1.0 / 64.0) / ffsd->oversampling * ffsd->scale; if (fd->msdf) { scale = scale * (double)p_size / (double)fd->msdf_source_size; } else if (fd->fixed_size > 0 && fd->fixed_size_scale_mode != FIXED_SIZE_SCALE_DISABLE && size.x != p_size) { @@ -3306,13 +3363,13 @@ Dictionary TextServerAdvanced::_font_get_glyph_contours(const RID &p_font_rid, i scale = scale * Math::round((double)p_size / (double)fd->fixed_size); } } - for (short i = 0; i < fd->cache[size]->face->glyph->outline.n_points; i++) { - points.push_back(Vector3(fd->cache[size]->face->glyph->outline.points[i].x * scale, -fd->cache[size]->face->glyph->outline.points[i].y * scale, FT_CURVE_TAG(fd->cache[size]->face->glyph->outline.tags[i]))); + for (short i = 0; i < ffsd->face->glyph->outline.n_points; i++) { + points.push_back(Vector3(ffsd->face->glyph->outline.points[i].x * scale, -ffsd->face->glyph->outline.points[i].y * scale, FT_CURVE_TAG(ffsd->face->glyph->outline.tags[i]))); } - for (short i = 0; i < fd->cache[size]->face->glyph->outline.n_contours; i++) { - contours.push_back(fd->cache[size]->face->glyph->outline.contours[i]); + for (short i = 0; i < ffsd->face->glyph->outline.n_contours; i++) { + contours.push_back(ffsd->face->glyph->outline.contours[i]); } - bool orientation = (FT_Outline_Get_Orientation(&fd->cache[size]->face->glyph->outline) == FT_ORIENTATION_FILL_RIGHT); + bool orientation = (FT_Outline_Get_Orientation(&ffsd->face->glyph->outline) == FT_ORIENTATION_FILL_RIGHT); Dictionary out; out["points"] = points; @@ -3331,7 +3388,8 @@ TypedArray<Vector2i> TextServerAdvanced::_font_get_kerning_list(const RID &p_fon MutexLock lock(fd->mutex); Vector2i size = _get_size(fd, p_size); - ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size), TypedArray<Vector2i>()); + FontForSizeAdvanced *ffsd = nullptr; + ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size, ffsd), TypedArray<Vector2i>()); TypedArray<Vector2i> ret; for (const KeyValue<Vector2i, Vector2> &E : fd->cache[size]->kerning_map) { @@ -3347,8 +3405,9 @@ void TextServerAdvanced::_font_clear_kerning_map(const RID &p_font_rid, int64_t MutexLock lock(fd->mutex); Vector2i size = _get_size(fd, p_size); - ERR_FAIL_COND(!_ensure_cache_for_size(fd, size)); - fd->cache[size]->kerning_map.clear(); + FontForSizeAdvanced *ffsd = nullptr; + ERR_FAIL_COND(!_ensure_cache_for_size(fd, size, ffsd)); + ffsd->kerning_map.clear(); } void TextServerAdvanced::_font_remove_kerning(const RID &p_font_rid, int64_t p_size, const Vector2i &p_glyph_pair) { @@ -3358,8 +3417,9 @@ void TextServerAdvanced::_font_remove_kerning(const RID &p_font_rid, int64_t p_s MutexLock lock(fd->mutex); Vector2i size = _get_size(fd, p_size); - ERR_FAIL_COND(!_ensure_cache_for_size(fd, size)); - fd->cache[size]->kerning_map.erase(p_glyph_pair); + FontForSizeAdvanced *ffsd = nullptr; + ERR_FAIL_COND(!_ensure_cache_for_size(fd, size, ffsd)); + ffsd->kerning_map.erase(p_glyph_pair); } void TextServerAdvanced::_font_set_kerning(const RID &p_font_rid, int64_t p_size, const Vector2i &p_glyph_pair, const Vector2 &p_kerning) { @@ -3369,8 +3429,9 @@ void TextServerAdvanced::_font_set_kerning(const RID &p_font_rid, int64_t p_size MutexLock lock(fd->mutex); Vector2i size = _get_size(fd, p_size); - ERR_FAIL_COND(!_ensure_cache_for_size(fd, size)); - fd->cache[size]->kerning_map[p_glyph_pair] = p_kerning; + FontForSizeAdvanced *ffsd = nullptr; + ERR_FAIL_COND(!_ensure_cache_for_size(fd, size, ffsd)); + ffsd->kerning_map[p_glyph_pair] = p_kerning; } Vector2 TextServerAdvanced::_font_get_kerning(const RID &p_font_rid, int64_t p_size, const Vector2i &p_glyph_pair) const { @@ -3380,9 +3441,10 @@ Vector2 TextServerAdvanced::_font_get_kerning(const RID &p_font_rid, int64_t p_s MutexLock lock(fd->mutex); Vector2i size = _get_size(fd, p_size); - ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size), Vector2()); + FontForSizeAdvanced *ffsd = nullptr; + ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size, ffsd), Vector2()); - const HashMap<Vector2i, Vector2> &kern = fd->cache[size]->kerning_map; + const HashMap<Vector2i, Vector2> &kern = ffsd->kerning_map; if (kern.has(p_glyph_pair)) { if (fd->msdf) { @@ -3398,9 +3460,9 @@ Vector2 TextServerAdvanced::_font_get_kerning(const RID &p_font_rid, int64_t p_s } } else { #ifdef MODULE_FREETYPE_ENABLED - if (fd->cache[size]->face) { + if (ffsd->face) { FT_Vector delta; - FT_Get_Kerning(fd->cache[size]->face, p_glyph_pair.x, p_glyph_pair.y, FT_KERNING_DEFAULT, &delta); + FT_Get_Kerning(ffsd->face, p_glyph_pair.x, p_glyph_pair.y, FT_KERNING_DEFAULT, &delta); if (fd->msdf) { return Vector2(delta.x, delta.y) * (double)p_size / (double)fd->msdf_source_size; } else if (fd->fixed_size > 0 && fd->fixed_size_scale_mode != FIXED_SIZE_SCALE_DISABLE && size.x != p_size) { @@ -3426,14 +3488,15 @@ int64_t TextServerAdvanced::_font_get_glyph_index(const RID &p_font_rid, int64_t MutexLock lock(fd->mutex); Vector2i size = _get_size(fd, p_size); - ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size), 0); + FontForSizeAdvanced *ffsd = nullptr; + ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size, ffsd), 0); #ifdef MODULE_FREETYPE_ENABLED - if (fd->cache[size]->face) { + if (ffsd->face) { if (p_variation_selector) { - return FT_Face_GetCharVariantIndex(fd->cache[size]->face, p_char, p_variation_selector); + return FT_Face_GetCharVariantIndex(ffsd->face, p_char, p_variation_selector); } else { - return FT_Get_Char_Index(fd->cache[size]->face, p_char); + return FT_Get_Char_Index(ffsd->face, p_char); } } else { return (int64_t)p_char; @@ -3449,23 +3512,24 @@ int64_t TextServerAdvanced::_font_get_char_from_glyph_index(const RID &p_font_ri MutexLock lock(fd->mutex); Vector2i size = _get_size(fd, p_size); - ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size), 0); + FontForSizeAdvanced *ffsd = nullptr; + ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size, ffsd), 0); #ifdef MODULE_FREETYPE_ENABLED - if (fd->cache[size]->inv_glyph_map.is_empty()) { - FT_Face face = fd->cache[size]->face; + if (ffsd->inv_glyph_map.is_empty()) { + FT_Face face = ffsd->face; FT_UInt gindex; FT_ULong charcode = FT_Get_First_Char(face, &gindex); while (gindex != 0) { if (charcode != 0) { - fd->cache[size]->inv_glyph_map[gindex] = charcode; + ffsd->inv_glyph_map[gindex] = charcode; } charcode = FT_Get_Next_Char(face, charcode, &gindex); } } - if (fd->cache[size]->inv_glyph_map.has(p_glyph_index)) { - return fd->cache[size]->inv_glyph_map[p_glyph_index]; + if (ffsd->inv_glyph_map.has(p_glyph_index)) { + return ffsd->inv_glyph_map[p_glyph_index]; } else { return 0; } @@ -3482,17 +3546,19 @@ bool TextServerAdvanced::_font_has_char(const RID &p_font_rid, int64_t p_char) c } MutexLock lock(fd->mutex); + FontForSizeAdvanced *ffsd = nullptr; if (fd->cache.is_empty()) { - ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, fd->msdf ? Vector2i(fd->msdf_source_size, 0) : Vector2i(16, 0)), false); + ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, fd->msdf ? Vector2i(fd->msdf_source_size, 0) : Vector2i(16, 0), ffsd), false); + } else { + ffsd = fd->cache.begin()->value; } - FontForSizeAdvanced *at_size = fd->cache.begin()->value; #ifdef MODULE_FREETYPE_ENABLED - if (at_size && at_size->face) { - return FT_Get_Char_Index(at_size->face, p_char) != 0; + if (ffsd->face) { + return FT_Get_Char_Index(ffsd->face, p_char) != 0; } #endif - return (at_size) ? at_size->glyph_map.has((int32_t)p_char) : false; + return ffsd->glyph_map.has((int32_t)p_char); } String TextServerAdvanced::_font_get_supported_chars(const RID &p_font_rid) const { @@ -3500,30 +3566,30 @@ String TextServerAdvanced::_font_get_supported_chars(const RID &p_font_rid) cons ERR_FAIL_NULL_V(fd, String()); MutexLock lock(fd->mutex); + FontForSizeAdvanced *ffsd = nullptr; if (fd->cache.is_empty()) { - ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, fd->msdf ? Vector2i(fd->msdf_source_size, 0) : Vector2i(16, 0)), String()); + ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, fd->msdf ? Vector2i(fd->msdf_source_size, 0) : Vector2i(16, 0), ffsd), String()); + } else { + ffsd = fd->cache.begin()->value; } - FontForSizeAdvanced *at_size = fd->cache.begin()->value; String chars; #ifdef MODULE_FREETYPE_ENABLED - if (at_size && at_size->face) { + if (ffsd->face) { FT_UInt gindex; - FT_ULong charcode = FT_Get_First_Char(at_size->face, &gindex); + FT_ULong charcode = FT_Get_First_Char(ffsd->face, &gindex); while (gindex != 0) { if (charcode != 0) { chars = chars + String::chr(charcode); } - charcode = FT_Get_Next_Char(at_size->face, charcode, &gindex); + charcode = FT_Get_Next_Char(ffsd->face, charcode, &gindex); } return chars; } #endif - if (at_size) { - const HashMap<int32_t, FontGlyph> &gl = at_size->glyph_map; - for (const KeyValue<int32_t, FontGlyph> &E : gl) { - chars = chars + String::chr(E.key); - } + const HashMap<int32_t, FontGlyph> &gl = ffsd->glyph_map; + for (const KeyValue<int32_t, FontGlyph> &E : gl) { + chars = chars + String::chr(E.key); } return chars; } @@ -3533,10 +3599,12 @@ PackedInt32Array TextServerAdvanced::_font_get_supported_glyphs(const RID &p_fon ERR_FAIL_NULL_V(fd, PackedInt32Array()); MutexLock lock(fd->mutex); + FontForSizeAdvanced *at_size = nullptr; if (fd->cache.is_empty()) { - ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, fd->msdf ? Vector2i(fd->msdf_source_size, 0) : Vector2i(16, 0)), PackedInt32Array()); + ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, fd->msdf ? Vector2i(fd->msdf_source_size, 0) : Vector2i(16, 0), at_size), PackedInt32Array()); + } else { + at_size = fd->cache.begin()->value; } - FontForSizeAdvanced *at_size = fd->cache.begin()->value; PackedInt32Array glyphs; #ifdef MODULE_FREETYPE_ENABLED @@ -3567,25 +3635,27 @@ void TextServerAdvanced::_font_render_range(const RID &p_font_rid, const Vector2 MutexLock lock(fd->mutex); Vector2i size = _get_size_outline(fd, p_size); - ERR_FAIL_COND(!_ensure_cache_for_size(fd, size)); + FontForSizeAdvanced *ffsd = nullptr; + ERR_FAIL_COND(!_ensure_cache_for_size(fd, size, ffsd)); for (int64_t i = p_start; i <= p_end; i++) { #ifdef MODULE_FREETYPE_ENABLED - int32_t idx = FT_Get_Char_Index(fd->cache[size]->face, i); - if (fd->cache[size]->face) { + int32_t idx = FT_Get_Char_Index(ffsd->face, i); + if (ffsd->face) { + FontGlyph fgl; if (fd->msdf) { - _ensure_glyph(fd, size, (int32_t)idx); + _ensure_glyph(fd, size, (int32_t)idx, fgl); } else { for (int aa = 0; aa < ((fd->antialiasing == FONT_ANTIALIASING_LCD) ? FONT_LCD_SUBPIXEL_LAYOUT_MAX : 1); aa++) { if ((fd->subpixel_positioning == SUBPIXEL_POSITIONING_ONE_QUARTER) || (fd->subpixel_positioning == SUBPIXEL_POSITIONING_AUTO && size.x <= SUBPIXEL_POSITIONING_ONE_QUARTER_MAX_SIZE)) { - _ensure_glyph(fd, size, (int32_t)idx | (0 << 27) | (aa << 24)); - _ensure_glyph(fd, size, (int32_t)idx | (1 << 27) | (aa << 24)); - _ensure_glyph(fd, size, (int32_t)idx | (2 << 27) | (aa << 24)); - _ensure_glyph(fd, size, (int32_t)idx | (3 << 27) | (aa << 24)); + _ensure_glyph(fd, size, (int32_t)idx | (0 << 27) | (aa << 24), fgl); + _ensure_glyph(fd, size, (int32_t)idx | (1 << 27) | (aa << 24), fgl); + _ensure_glyph(fd, size, (int32_t)idx | (2 << 27) | (aa << 24), fgl); + _ensure_glyph(fd, size, (int32_t)idx | (3 << 27) | (aa << 24), fgl); } else if ((fd->subpixel_positioning == SUBPIXEL_POSITIONING_ONE_HALF) || (fd->subpixel_positioning == SUBPIXEL_POSITIONING_AUTO && size.x <= SUBPIXEL_POSITIONING_ONE_HALF_MAX_SIZE)) { - _ensure_glyph(fd, size, (int32_t)idx | (1 << 27) | (aa << 24)); - _ensure_glyph(fd, size, (int32_t)idx | (0 << 27) | (aa << 24)); + _ensure_glyph(fd, size, (int32_t)idx | (1 << 27) | (aa << 24), fgl); + _ensure_glyph(fd, size, (int32_t)idx | (0 << 27) | (aa << 24), fgl); } else { - _ensure_glyph(fd, size, (int32_t)idx | (aa << 24)); + _ensure_glyph(fd, size, (int32_t)idx | (aa << 24), fgl); } } } @@ -3600,24 +3670,26 @@ void TextServerAdvanced::_font_render_glyph(const RID &p_font_rid, const Vector2 MutexLock lock(fd->mutex); Vector2i size = _get_size_outline(fd, p_size); - ERR_FAIL_COND(!_ensure_cache_for_size(fd, size)); + FontForSizeAdvanced *ffsd = nullptr; + ERR_FAIL_COND(!_ensure_cache_for_size(fd, size, ffsd)); #ifdef MODULE_FREETYPE_ENABLED int32_t idx = p_index & 0xffffff; // Remove subpixel shifts. - if (fd->cache[size]->face) { + if (ffsd->face) { + FontGlyph fgl; if (fd->msdf) { - _ensure_glyph(fd, size, (int32_t)idx); + _ensure_glyph(fd, size, (int32_t)idx, fgl); } else { for (int aa = 0; aa < ((fd->antialiasing == FONT_ANTIALIASING_LCD) ? FONT_LCD_SUBPIXEL_LAYOUT_MAX : 1); aa++) { if ((fd->subpixel_positioning == SUBPIXEL_POSITIONING_ONE_QUARTER) || (fd->subpixel_positioning == SUBPIXEL_POSITIONING_AUTO && size.x <= SUBPIXEL_POSITIONING_ONE_QUARTER_MAX_SIZE)) { - _ensure_glyph(fd, size, (int32_t)idx | (0 << 27) | (aa << 24)); - _ensure_glyph(fd, size, (int32_t)idx | (1 << 27) | (aa << 24)); - _ensure_glyph(fd, size, (int32_t)idx | (2 << 27) | (aa << 24)); - _ensure_glyph(fd, size, (int32_t)idx | (3 << 27) | (aa << 24)); + _ensure_glyph(fd, size, (int32_t)idx | (0 << 27) | (aa << 24), fgl); + _ensure_glyph(fd, size, (int32_t)idx | (1 << 27) | (aa << 24), fgl); + _ensure_glyph(fd, size, (int32_t)idx | (2 << 27) | (aa << 24), fgl); + _ensure_glyph(fd, size, (int32_t)idx | (3 << 27) | (aa << 24), fgl); } else if ((fd->subpixel_positioning == SUBPIXEL_POSITIONING_ONE_HALF) || (fd->subpixel_positioning == SUBPIXEL_POSITIONING_AUTO && size.x <= SUBPIXEL_POSITIONING_ONE_HALF_MAX_SIZE)) { - _ensure_glyph(fd, size, (int32_t)idx | (1 << 27) | (aa << 24)); - _ensure_glyph(fd, size, (int32_t)idx | (0 << 27) | (aa << 24)); + _ensure_glyph(fd, size, (int32_t)idx | (1 << 27) | (aa << 24), fgl); + _ensure_glyph(fd, size, (int32_t)idx | (0 << 27) | (aa << 24), fgl); } else { - _ensure_glyph(fd, size, (int32_t)idx | (aa << 24)); + _ensure_glyph(fd, size, (int32_t)idx | (aa << 24), fgl); } } } @@ -3634,16 +3706,17 @@ void TextServerAdvanced::_font_draw_glyph(const RID &p_font_rid, const RID &p_ca MutexLock lock(fd->mutex); Vector2i size = _get_size(fd, p_size); - ERR_FAIL_COND(!_ensure_cache_for_size(fd, size)); + FontForSizeAdvanced *ffsd = nullptr; + ERR_FAIL_COND(!_ensure_cache_for_size(fd, size, ffsd)); int32_t index = p_index & 0xffffff; // Remove subpixel shifts. bool lcd_aa = false; #ifdef MODULE_FREETYPE_ENABLED - if (!fd->msdf && fd->cache[size]->face) { + if (!fd->msdf && ffsd->face) { // LCD layout, bits 24, 25, 26 if (fd->antialiasing == FONT_ANTIALIASING_LCD) { - TextServer::FontLCDSubpixelLayout layout = (TextServer::FontLCDSubpixelLayout)(int)GLOBAL_GET("gui/theme/lcd_subpixel_layout"); + TextServer::FontLCDSubpixelLayout layout = lcd_subpixel_layout.get(); if (layout != FONT_LCD_SUBPIXEL_LAYOUT_NONE) { lcd_aa = true; index = index | (layout << 24); @@ -3660,24 +3733,24 @@ void TextServerAdvanced::_font_draw_glyph(const RID &p_font_rid, const RID &p_ca } #endif - if (!_ensure_glyph(fd, size, index)) { + FontGlyph fgl; + if (!_ensure_glyph(fd, size, index, fgl)) { return; // Invalid or non-graphical glyph, do not display errors, nothing to draw. } - const FontGlyph &gl = fd->cache[size]->glyph_map[index]; - if (gl.found) { - ERR_FAIL_COND(gl.texture_idx < -1 || gl.texture_idx >= fd->cache[size]->textures.size()); + if (fgl.found) { + ERR_FAIL_COND(fgl.texture_idx < -1 || fgl.texture_idx >= ffsd->textures.size()); - if (gl.texture_idx != -1) { + if (fgl.texture_idx != -1) { Color modulate = p_color; #ifdef MODULE_FREETYPE_ENABLED - if (fd->cache[size]->face && fd->cache[size]->textures[gl.texture_idx].image.is_valid() && (fd->cache[size]->textures[gl.texture_idx].image->get_format() == Image::FORMAT_RGBA8) && !lcd_aa && !fd->msdf) { + if (ffsd->face && ffsd->textures[fgl.texture_idx].image.is_valid() && (ffsd->textures[fgl.texture_idx].image->get_format() == Image::FORMAT_RGBA8) && !lcd_aa && !fd->msdf) { modulate.r = modulate.g = modulate.b = 1.0; } #endif if (RenderingServer::get_singleton() != nullptr) { - if (fd->cache[size]->textures[gl.texture_idx].dirty) { - ShelfPackTexture &tex = fd->cache[size]->textures.write[gl.texture_idx]; + if (ffsd->textures[fgl.texture_idx].dirty) { + ShelfPackTexture &tex = ffsd->textures.write[fgl.texture_idx]; Ref<Image> img = tex.image; if (fd->mipmaps && !img->has_mipmaps()) { img = tex.image->duplicate(); @@ -3690,12 +3763,12 @@ void TextServerAdvanced::_font_draw_glyph(const RID &p_font_rid, const RID &p_ca } tex.dirty = false; } - RID texture = fd->cache[size]->textures[gl.texture_idx].texture->get_rid(); + RID texture = ffsd->textures[fgl.texture_idx].texture->get_rid(); if (fd->msdf) { Point2 cpos = p_pos; - cpos += gl.rect.position * (double)p_size / (double)fd->msdf_source_size; - Size2 csize = gl.rect.size * (double)p_size / (double)fd->msdf_source_size; - RenderingServer::get_singleton()->canvas_item_add_msdf_texture_rect_region(p_canvas, Rect2(cpos, csize), texture, gl.uv_rect, modulate, 0, fd->msdf_range, (double)p_size / (double)fd->msdf_source_size); + cpos += fgl.rect.position * (double)p_size / (double)fd->msdf_source_size; + Size2 csize = fgl.rect.size * (double)p_size / (double)fd->msdf_source_size; + RenderingServer::get_singleton()->canvas_item_add_msdf_texture_rect_region(p_canvas, Rect2(cpos, csize), texture, fgl.uv_rect, modulate, 0, fd->msdf_range, (double)p_size / (double)fd->msdf_source_size); } else { double scale = _font_get_scale(p_font_rid, p_size); Point2 cpos = p_pos; @@ -3708,8 +3781,8 @@ void TextServerAdvanced::_font_draw_glyph(const RID &p_font_rid, const RID &p_ca cpos.y = Math::floor(cpos.y); cpos.x = Math::floor(cpos.x); } - Vector2 gpos = gl.rect.position; - Size2 csize = gl.rect.size; + Vector2 gpos = fgl.rect.position; + Size2 csize = fgl.rect.size; if (fd->fixed_size > 0 && fd->fixed_size_scale_mode != FIXED_SIZE_SCALE_DISABLE && size.x != p_size) { if (fd->fixed_size_scale_mode == FIXED_SIZE_SCALE_ENABLED) { double gl_scale = (double)p_size / (double)fd->fixed_size; @@ -3723,9 +3796,9 @@ void TextServerAdvanced::_font_draw_glyph(const RID &p_font_rid, const RID &p_ca } cpos += gpos; if (lcd_aa) { - RenderingServer::get_singleton()->canvas_item_add_lcd_texture_rect_region(p_canvas, Rect2(cpos, csize), texture, gl.uv_rect, modulate); + RenderingServer::get_singleton()->canvas_item_add_lcd_texture_rect_region(p_canvas, Rect2(cpos, csize), texture, fgl.uv_rect, modulate); } else { - RenderingServer::get_singleton()->canvas_item_add_texture_rect_region(p_canvas, Rect2(cpos, csize), texture, gl.uv_rect, modulate, false, false); + RenderingServer::get_singleton()->canvas_item_add_texture_rect_region(p_canvas, Rect2(cpos, csize), texture, fgl.uv_rect, modulate, false, false); } } } @@ -3742,16 +3815,17 @@ void TextServerAdvanced::_font_draw_glyph_outline(const RID &p_font_rid, const R MutexLock lock(fd->mutex); Vector2i size = _get_size_outline(fd, Vector2i(p_size, p_outline_size)); - ERR_FAIL_COND(!_ensure_cache_for_size(fd, size)); + FontForSizeAdvanced *ffsd = nullptr; + ERR_FAIL_COND(!_ensure_cache_for_size(fd, size, ffsd)); int32_t index = p_index & 0xffffff; // Remove subpixel shifts. bool lcd_aa = false; #ifdef MODULE_FREETYPE_ENABLED - if (!fd->msdf && fd->cache[size]->face) { + if (!fd->msdf && ffsd->face) { // LCD layout, bits 24, 25, 26 if (fd->antialiasing == FONT_ANTIALIASING_LCD) { - TextServer::FontLCDSubpixelLayout layout = (TextServer::FontLCDSubpixelLayout)(int)GLOBAL_GET("gui/theme/lcd_subpixel_layout"); + TextServer::FontLCDSubpixelLayout layout = lcd_subpixel_layout.get(); if (layout != FONT_LCD_SUBPIXEL_LAYOUT_NONE) { lcd_aa = true; index = index | (layout << 24); @@ -3768,24 +3842,24 @@ void TextServerAdvanced::_font_draw_glyph_outline(const RID &p_font_rid, const R } #endif - if (!_ensure_glyph(fd, size, index)) { + FontGlyph fgl; + if (!_ensure_glyph(fd, size, index, fgl)) { return; // Invalid or non-graphical glyph, do not display errors, nothing to draw. } - const FontGlyph &gl = fd->cache[size]->glyph_map[index]; - if (gl.found) { - ERR_FAIL_COND(gl.texture_idx < -1 || gl.texture_idx >= fd->cache[size]->textures.size()); + if (fgl.found) { + ERR_FAIL_COND(fgl.texture_idx < -1 || fgl.texture_idx >= ffsd->textures.size()); - if (gl.texture_idx != -1) { + if (fgl.texture_idx != -1) { Color modulate = p_color; #ifdef MODULE_FREETYPE_ENABLED - if (fd->cache[size]->face && fd->cache[size]->textures[gl.texture_idx].image.is_valid() && (fd->cache[size]->textures[gl.texture_idx].image->get_format() == Image::FORMAT_RGBA8) && !lcd_aa && !fd->msdf) { + if (ffsd->face && fd->cache[size]->textures[fgl.texture_idx].image.is_valid() && (ffsd->textures[fgl.texture_idx].image->get_format() == Image::FORMAT_RGBA8) && !lcd_aa && !fd->msdf) { modulate.r = modulate.g = modulate.b = 1.0; } #endif if (RenderingServer::get_singleton() != nullptr) { - if (fd->cache[size]->textures[gl.texture_idx].dirty) { - ShelfPackTexture &tex = fd->cache[size]->textures.write[gl.texture_idx]; + if (ffsd->textures[fgl.texture_idx].dirty) { + ShelfPackTexture &tex = ffsd->textures.write[fgl.texture_idx]; Ref<Image> img = tex.image; if (fd->mipmaps && !img->has_mipmaps()) { img = tex.image->duplicate(); @@ -3798,12 +3872,12 @@ void TextServerAdvanced::_font_draw_glyph_outline(const RID &p_font_rid, const R } tex.dirty = false; } - RID texture = fd->cache[size]->textures[gl.texture_idx].texture->get_rid(); + RID texture = ffsd->textures[fgl.texture_idx].texture->get_rid(); if (fd->msdf) { Point2 cpos = p_pos; - cpos += gl.rect.position * (double)p_size / (double)fd->msdf_source_size; - Size2 csize = gl.rect.size * (double)p_size / (double)fd->msdf_source_size; - RenderingServer::get_singleton()->canvas_item_add_msdf_texture_rect_region(p_canvas, Rect2(cpos, csize), texture, gl.uv_rect, modulate, p_outline_size, fd->msdf_range, (double)p_size / (double)fd->msdf_source_size); + cpos += fgl.rect.position * (double)p_size / (double)fd->msdf_source_size; + Size2 csize = fgl.rect.size * (double)p_size / (double)fd->msdf_source_size; + RenderingServer::get_singleton()->canvas_item_add_msdf_texture_rect_region(p_canvas, Rect2(cpos, csize), texture, fgl.uv_rect, modulate, p_outline_size, fd->msdf_range, (double)p_size / (double)fd->msdf_source_size); } else { Point2 cpos = p_pos; double scale = _font_get_scale(p_font_rid, p_size); @@ -3816,8 +3890,8 @@ void TextServerAdvanced::_font_draw_glyph_outline(const RID &p_font_rid, const R cpos.y = Math::floor(cpos.y); cpos.x = Math::floor(cpos.x); } - Vector2 gpos = gl.rect.position; - Size2 csize = gl.rect.size; + Vector2 gpos = fgl.rect.position; + Size2 csize = fgl.rect.size; if (fd->fixed_size > 0 && fd->fixed_size_scale_mode != FIXED_SIZE_SCALE_DISABLE && size.x != p_size) { if (fd->fixed_size_scale_mode == FIXED_SIZE_SCALE_ENABLED) { double gl_scale = (double)p_size / (double)fd->fixed_size; @@ -3831,9 +3905,9 @@ void TextServerAdvanced::_font_draw_glyph_outline(const RID &p_font_rid, const R } cpos += gpos; if (lcd_aa) { - RenderingServer::get_singleton()->canvas_item_add_lcd_texture_rect_region(p_canvas, Rect2(cpos, csize), texture, gl.uv_rect, modulate); + RenderingServer::get_singleton()->canvas_item_add_lcd_texture_rect_region(p_canvas, Rect2(cpos, csize), texture, fgl.uv_rect, modulate); } else { - RenderingServer::get_singleton()->canvas_item_add_texture_rect_region(p_canvas, Rect2(cpos, csize), texture, gl.uv_rect, modulate, false, false); + RenderingServer::get_singleton()->canvas_item_add_texture_rect_region(p_canvas, Rect2(cpos, csize), texture, fgl.uv_rect, modulate, false, false); } } } @@ -3898,7 +3972,8 @@ bool TextServerAdvanced::_font_is_script_supported(const RID &p_font_rid, const return fd->script_support_overrides[p_script]; } else { Vector2i size = _get_size(fd, 16); - ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size), false); + FontForSizeAdvanced *ffsd = nullptr; + ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size, ffsd), false); return fd->supported_scripts.has(hb_tag_from_string(p_script.ascii().get_data(), -1)); } } @@ -3945,7 +4020,8 @@ void TextServerAdvanced::_font_set_opentype_feature_overrides(const RID &p_font_ MutexLock lock(fd->mutex); Vector2i size = _get_size(fd, 16); - ERR_FAIL_COND(!_ensure_cache_for_size(fd, size)); + FontForSizeAdvanced *ffsd = nullptr; + ERR_FAIL_COND(!_ensure_cache_for_size(fd, size, ffsd)); fd->feature_overrides = p_overrides; } @@ -3963,7 +4039,8 @@ Dictionary TextServerAdvanced::_font_supported_feature_list(const RID &p_font_ri MutexLock lock(fd->mutex); Vector2i size = _get_size(fd, 16); - ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size), Dictionary()); + FontForSizeAdvanced *ffsd = nullptr; + ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size, ffsd), Dictionary()); return fd->supported_features; } @@ -3973,7 +4050,8 @@ Dictionary TextServerAdvanced::_font_supported_variation_list(const RID &p_font_ MutexLock lock(fd->mutex); Vector2i size = _get_size(fd, 16); - ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size), Dictionary()); + FontForSizeAdvanced *ffsd = nullptr; + ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size, ffsd), Dictionary()); return fd->supported_varaitions; } @@ -4048,7 +4126,7 @@ int64_t TextServerAdvanced::_convert_pos_inv(const ShapedTextDataAdvanced *p_sd, } void TextServerAdvanced::invalidate(TextServerAdvanced::ShapedTextDataAdvanced *p_shaped, bool p_text) { - p_shaped->valid = false; + p_shaped->valid.clear(); p_shaped->sort_valid = false; p_shaped->line_breaks_valid = false; p_shaped->justification_ops_valid = false; @@ -4404,7 +4482,7 @@ bool TextServerAdvanced::_shaped_text_resize_object(const RID &p_shaped, const V sd->objects[p_key].rect.size = p_size; sd->objects[p_key].inline_align = p_inline_align; sd->objects[p_key].baseline = p_baseline; - if (sd->valid) { + if (sd->valid.is_set()) { // Recalc string metrics. sd->ascent = 0; sd->descent = 0; @@ -4548,7 +4626,7 @@ RID TextServerAdvanced::_shaped_text_substr(const RID &p_shaped, int64_t p_start if (sd->parent != RID()) { return _shaped_text_substr(sd->parent, p_start, p_length); } - if (!sd->valid) { + if (!sd->valid.is_set()) { const_cast<TextServerAdvanced *>(this)->_shaped_text_shape(p_shaped); } ERR_FAIL_COND_V(p_start < 0 || p_length < 0, RID()); @@ -4576,7 +4654,7 @@ RID TextServerAdvanced::_shaped_text_substr(const RID &p_shaped, int64_t p_start } bool TextServerAdvanced::_shape_substr(ShapedTextDataAdvanced *p_new_sd, const ShapedTextDataAdvanced *p_sd, int64_t p_start, int64_t p_length) const { - if (p_new_sd->valid) { + if (p_new_sd->valid.is_set()) { return true; } @@ -4725,7 +4803,7 @@ bool TextServerAdvanced::_shape_substr(ShapedTextDataAdvanced *p_new_sd, const S _realign(p_new_sd); } - p_new_sd->valid = true; + p_new_sd->valid.set(); return true; } @@ -4743,7 +4821,7 @@ double TextServerAdvanced::_shaped_text_fit_to_width(const RID &p_shaped, double ERR_FAIL_NULL_V(sd, 0.0); MutexLock lock(sd->mutex); - if (!sd->valid) { + if (!sd->valid.is_set()) { const_cast<TextServerAdvanced *>(this)->_shaped_text_shape(p_shaped); } if (!sd->justification_ops_valid) { @@ -4900,7 +4978,7 @@ double TextServerAdvanced::_shaped_text_tab_align(const RID &p_shaped, const Pac ERR_FAIL_NULL_V(sd, 0.0); MutexLock lock(sd->mutex); - if (!sd->valid) { + if (!sd->valid.is_set()) { const_cast<TextServerAdvanced *>(this)->_shaped_text_shape(p_shaped); } if (!sd->line_breaks_valid) { @@ -5117,7 +5195,7 @@ void TextServerAdvanced::_shaped_text_overrun_trim_to_width(const RID &p_shaped_ ERR_FAIL_NULL_MSG(sd, "ShapedTextDataAdvanced invalid."); MutexLock lock(sd->mutex); - if (!sd->valid) { + if (!sd->valid.is_set()) { _shaped_text_shape(p_shaped_line); } @@ -5144,7 +5222,7 @@ void TextServerAdvanced::_shaped_text_overrun_trim_to_width(const RID &p_shaped_ Vector<ShapedTextDataAdvanced::Span> &spans = sd->spans; if (sd->parent != RID()) { ShapedTextDataAdvanced *parent_sd = shaped_owner.get_or_null(sd->parent); - ERR_FAIL_COND(!parent_sd->valid); + ERR_FAIL_COND(!parent_sd->valid.is_set()); spans = parent_sd->spans; } @@ -5355,7 +5433,7 @@ void TextServerAdvanced::_update_chars(ShapedTextDataAdvanced *p_sd) const { Vector<ShapedTextDataAdvanced::Span> &spans = p_sd->spans; if (p_sd->parent != RID()) { ShapedTextDataAdvanced *parent_sd = shaped_owner.get_or_null(p_sd->parent); - ERR_FAIL_COND(!parent_sd->valid); + ERR_FAIL_COND(!parent_sd->valid.is_set()); spans = parent_sd->spans; } @@ -5403,7 +5481,7 @@ PackedInt32Array TextServerAdvanced::_shaped_text_get_character_breaks(const RID ERR_FAIL_NULL_V(sd, PackedInt32Array()); MutexLock lock(sd->mutex); - if (!sd->valid) { + if (!sd->valid.is_set()) { const_cast<TextServerAdvanced *>(this)->_shaped_text_shape(p_shaped); } @@ -5417,7 +5495,7 @@ bool TextServerAdvanced::_shaped_text_update_breaks(const RID &p_shaped) { ERR_FAIL_NULL_V(sd, false); MutexLock lock(sd->mutex); - if (!sd->valid) { + if (!sd->valid.is_set()) { _shaped_text_shape(p_shaped); } @@ -5688,7 +5766,7 @@ bool TextServerAdvanced::_shaped_text_update_justification_ops(const RID &p_shap ERR_FAIL_NULL_V(sd, false); MutexLock lock(sd->mutex); - if (!sd->valid) { + if (!sd->valid.is_set()) { _shaped_text_shape(p_shaped); } if (!sd->line_breaks_valid) { @@ -6068,7 +6146,7 @@ void TextServerAdvanced::_shape_run(ShapedTextDataAdvanced *p_sd, int64_t p_star int mod = 0; if (fd->antialiasing == FONT_ANTIALIASING_LCD) { - TextServer::FontLCDSubpixelLayout layout = (TextServer::FontLCDSubpixelLayout)(int)GLOBAL_GET("gui/theme/lcd_subpixel_layout"); + TextServer::FontLCDSubpixelLayout layout = lcd_subpixel_layout.get(); if (layout != FONT_LCD_SUBPIXEL_LAYOUT_NONE) { mod = (layout << 24); } @@ -6128,7 +6206,8 @@ void TextServerAdvanced::_shape_run(ShapedTextDataAdvanced *p_sd, int64_t p_star gl.index = glyph_info[i].codepoint; if (gl.index != 0) { - _ensure_glyph(fd, fss, gl.index | mod); + FontGlyph fgl; + _ensure_glyph(fd, fss, gl.index | mod, fgl); if (subpos) { gl.x_off = (double)glyph_pos[i].x_offset / (64.0 / scale); } else if (p_sd->orientation == ORIENTATION_HORIZONTAL) { @@ -6240,7 +6319,7 @@ bool TextServerAdvanced::_shaped_text_shape(const RID &p_shaped) { ERR_FAIL_NULL_V(sd, false); MutexLock lock(sd->mutex); - if (sd->valid) { + if (sd->valid.is_set()) { return true; } @@ -6248,13 +6327,13 @@ bool TextServerAdvanced::_shaped_text_shape(const RID &p_shaped) { if (sd->parent != RID()) { _shaped_text_shape(sd->parent); ShapedTextDataAdvanced *parent_sd = shaped_owner.get_or_null(sd->parent); - ERR_FAIL_COND_V(!parent_sd->valid, false); + ERR_FAIL_COND_V(!parent_sd->valid.is_set(), false); ERR_FAIL_COND_V(!_shape_substr(sd, parent_sd, sd->start, sd->end - sd->start), false); return true; } if (sd->text.length() == 0) { - sd->valid = true; + sd->valid.set(); return true; } @@ -6447,16 +6526,16 @@ bool TextServerAdvanced::_shaped_text_shape(const RID &p_shaped) { } _realign(sd); - sd->valid = true; - return sd->valid; + sd->valid.set(); + return sd->valid.is_set(); } bool TextServerAdvanced::_shaped_text_is_ready(const RID &p_shaped) const { const ShapedTextDataAdvanced *sd = shaped_owner.get_or_null(p_shaped); ERR_FAIL_NULL_V(sd, false); - MutexLock lock(sd->mutex); - return sd->valid; + // Atomic read is safe and faster. + return sd->valid.is_set(); } const Glyph *TextServerAdvanced::_shaped_text_get_glyphs(const RID &p_shaped) const { @@ -6464,7 +6543,7 @@ const Glyph *TextServerAdvanced::_shaped_text_get_glyphs(const RID &p_shaped) co ERR_FAIL_NULL_V(sd, nullptr); MutexLock lock(sd->mutex); - if (!sd->valid) { + if (!sd->valid.is_set()) { const_cast<TextServerAdvanced *>(this)->_shaped_text_shape(p_shaped); } return sd->glyphs.ptr(); @@ -6475,7 +6554,7 @@ int64_t TextServerAdvanced::_shaped_text_get_glyph_count(const RID &p_shaped) co ERR_FAIL_NULL_V(sd, 0); MutexLock lock(sd->mutex); - if (!sd->valid) { + if (!sd->valid.is_set()) { const_cast<TextServerAdvanced *>(this)->_shaped_text_shape(p_shaped); } return sd->glyphs.size(); @@ -6486,7 +6565,7 @@ const Glyph *TextServerAdvanced::_shaped_text_sort_logical(const RID &p_shaped) ERR_FAIL_NULL_V(sd, nullptr); MutexLock lock(sd->mutex); - if (!sd->valid) { + if (!sd->valid.is_set()) { const_cast<TextServerAdvanced *>(this)->_shaped_text_shape(p_shaped); } @@ -6526,7 +6605,7 @@ Rect2 TextServerAdvanced::_shaped_text_get_object_rect(const RID &p_shaped, cons MutexLock lock(sd->mutex); ERR_FAIL_COND_V(!sd->objects.has(p_key), Rect2()); - if (!sd->valid) { + if (!sd->valid.is_set()) { const_cast<TextServerAdvanced *>(this)->_shaped_text_shape(p_shaped); } return sd->objects[p_key].rect; @@ -6547,7 +6626,7 @@ int64_t TextServerAdvanced::_shaped_text_get_object_glyph(const RID &p_shaped, c MutexLock lock(sd->mutex); ERR_FAIL_COND_V(!sd->objects.has(p_key), -1); - if (!sd->valid) { + if (!sd->valid.is_set()) { const_cast<TextServerAdvanced *>(this)->_shaped_text_shape(p_shaped); } const ShapedTextDataAdvanced::EmbeddedObject &obj = sd->objects[p_key]; @@ -6566,7 +6645,7 @@ Size2 TextServerAdvanced::_shaped_text_get_size(const RID &p_shaped) const { ERR_FAIL_NULL_V(sd, Size2()); MutexLock lock(sd->mutex); - if (!sd->valid) { + if (!sd->valid.is_set()) { const_cast<TextServerAdvanced *>(this)->_shaped_text_shape(p_shaped); } if (sd->orientation == TextServer::ORIENTATION_HORIZONTAL) { @@ -6581,7 +6660,7 @@ double TextServerAdvanced::_shaped_text_get_ascent(const RID &p_shaped) const { ERR_FAIL_NULL_V(sd, 0.0); MutexLock lock(sd->mutex); - if (!sd->valid) { + if (!sd->valid.is_set()) { const_cast<TextServerAdvanced *>(this)->_shaped_text_shape(p_shaped); } return sd->ascent + sd->extra_spacing[SPACING_TOP]; @@ -6592,7 +6671,7 @@ double TextServerAdvanced::_shaped_text_get_descent(const RID &p_shaped) const { ERR_FAIL_NULL_V(sd, 0.0); MutexLock lock(sd->mutex); - if (!sd->valid) { + if (!sd->valid.is_set()) { const_cast<TextServerAdvanced *>(this)->_shaped_text_shape(p_shaped); } return sd->descent + sd->extra_spacing[SPACING_BOTTOM]; @@ -6603,7 +6682,7 @@ double TextServerAdvanced::_shaped_text_get_width(const RID &p_shaped) const { ERR_FAIL_NULL_V(sd, 0.0); MutexLock lock(sd->mutex); - if (!sd->valid) { + if (!sd->valid.is_set()) { const_cast<TextServerAdvanced *>(this)->_shaped_text_shape(p_shaped); } return Math::ceil(sd->text_trimmed ? sd->width_trimmed : sd->width); @@ -6614,7 +6693,7 @@ double TextServerAdvanced::_shaped_text_get_underline_position(const RID &p_shap ERR_FAIL_NULL_V(sd, 0.0); MutexLock lock(sd->mutex); - if (!sd->valid) { + if (!sd->valid.is_set()) { const_cast<TextServerAdvanced *>(this)->_shaped_text_shape(p_shaped); } @@ -6626,7 +6705,7 @@ double TextServerAdvanced::_shaped_text_get_underline_thickness(const RID &p_sha ERR_FAIL_NULL_V(sd, 0.0); MutexLock lock(sd->mutex); - if (!sd->valid) { + if (!sd->valid.is_set()) { const_cast<TextServerAdvanced *>(this)->_shaped_text_shape(p_shaped); } @@ -7424,10 +7503,15 @@ bool TextServerAdvanced::_is_valid_letter(uint64_t p_unicode) const { return u_isalpha(p_unicode); } +void TextServerAdvanced::_update_settings() { + lcd_subpixel_layout.set((TextServer::FontLCDSubpixelLayout)(int)GLOBAL_GET("gui/theme/lcd_subpixel_layout")); +} + TextServerAdvanced::TextServerAdvanced() { _insert_num_systems_lang(); _insert_feature_sets(); _bmp_create_font_funcs(); + ProjectSettings::get_singleton()->connect("settings_changed", callable_mp(this, &TextServerAdvanced::_update_settings)); } void TextServerAdvanced::_cleanup() { diff --git a/modules/text_server_adv/text_server_adv.h b/modules/text_server_adv/text_server_adv.h index fdebb8e4cd..448be9ebe4 100644 --- a/modules/text_server_adv/text_server_adv.h +++ b/modules/text_server_adv/text_server_adv.h @@ -74,6 +74,7 @@ #include <godot_cpp/templates/hash_map.hpp> #include <godot_cpp/templates/hash_set.hpp> #include <godot_cpp/templates/rid_owner.hpp> +#include <godot_cpp/templates/safe_refcount.hpp> #include <godot_cpp/templates/vector.hpp> using namespace godot; @@ -85,6 +86,7 @@ using namespace godot; #include "core/object/worker_thread_pool.h" #include "core/templates/hash_map.h" #include "core/templates/rid_owner.h" +#include "core/templates/safe_refcount.h" #include "scene/resources/image_texture.h" #include "servers/text/text_server_extension.h" @@ -151,6 +153,9 @@ class TextServerAdvanced : public TextServerExtension { HashMap<StringName, int32_t> feature_sets; HashMap<int32_t, FeatureInfo> feature_sets_inv; + SafeNumeric<TextServer::FontLCDSubpixelLayout> lcd_subpixel_layout{ TextServer::FontLCDSubpixelLayout::FONT_LCD_SUBPIXEL_LAYOUT_NONE }; + void _update_settings(); + void _insert_num_systems_lang(); void _insert_feature_sets(); _FORCE_INLINE_ void _insert_feature(const StringName &p_name, int32_t p_tag, Variant::Type p_vtype = Variant::INT, bool p_hidden = false); @@ -327,7 +332,7 @@ class TextServerAdvanced : public TextServerExtension { int extra_spacing[4] = { 0, 0, 0, 0 }; double baseline_offset = 0.0; - HashMap<Vector2i, FontForSizeAdvanced *, VariantHasher, VariantComparator> cache; + HashMap<Vector2i, FontForSizeAdvanced *> cache; bool face_init = false; HashSet<uint32_t> supported_scripts; @@ -359,8 +364,8 @@ class TextServerAdvanced : public TextServerExtension { #ifdef MODULE_FREETYPE_ENABLED _FORCE_INLINE_ FontGlyph rasterize_bitmap(FontForSizeAdvanced *p_data, int p_rect_margin, FT_Bitmap p_bitmap, int p_yofs, int p_xofs, const Vector2 &p_advance, bool p_bgra) const; #endif - _FORCE_INLINE_ bool _ensure_glyph(FontAdvanced *p_font_data, const Vector2i &p_size, int32_t p_glyph) const; - _FORCE_INLINE_ bool _ensure_cache_for_size(FontAdvanced *p_font_data, const Vector2i &p_size) const; + _FORCE_INLINE_ bool _ensure_glyph(FontAdvanced *p_font_data, const Vector2i &p_size, int32_t p_glyph, FontGlyph &r_glyph) const; + _FORCE_INLINE_ bool _ensure_cache_for_size(FontAdvanced *p_font_data, const Vector2i &p_size, FontForSizeAdvanced *&r_cache_for_size) const; _FORCE_INLINE_ void _font_clear_cache(FontAdvanced *p_font_data); static void _generateMTSDF_threaded(void *p_td, uint32_t p_y); @@ -487,7 +492,7 @@ class TextServerAdvanced : public TextServerExtension { /* Shaped data */ TextServer::Direction para_direction = DIRECTION_LTR; // Detected text direction. int base_para_direction = UBIDI_DEFAULT_LTR; - bool valid = false; // String is shaped. + SafeFlag valid{ false }; // String is shaped. bool line_breaks_valid = false; // Line and word break flags are populated (and virtual zero width spaces inserted). bool justification_ops_valid = false; // Virtual elongation glyphs are added to the string. bool sort_valid = false; diff --git a/modules/text_server_fb/text_server_fb.cpp b/modules/text_server_fb/text_server_fb.cpp index a7ddfc719e..540ba19cac 100644 --- a/modules/text_server_fb/text_server_fb.cpp +++ b/modules/text_server_fb/text_server_fb.cpp @@ -624,18 +624,21 @@ _FORCE_INLINE_ TextServerFallback::FontGlyph TextServerFallback::rasterize_bitma /* Font Cache */ /*************************************************************************/ -_FORCE_INLINE_ bool TextServerFallback::_ensure_glyph(FontFallback *p_font_data, const Vector2i &p_size, int32_t p_glyph) const { - ERR_FAIL_COND_V(!_ensure_cache_for_size(p_font_data, p_size), false); +_FORCE_INLINE_ bool TextServerFallback::_ensure_glyph(FontFallback *p_font_data, const Vector2i &p_size, int32_t p_glyph, FontGlyph &r_glyph) const { + FontForSizeFallback *fd = nullptr; + ERR_FAIL_COND_V(!_ensure_cache_for_size(p_font_data, p_size, fd), false); int32_t glyph_index = p_glyph & 0xffffff; // Remove subpixel shifts. - FontForSizeFallback *fd = p_font_data->cache[p_size]; - if (fd->glyph_map.has(p_glyph)) { - return fd->glyph_map[p_glyph].found; + HashMap<int32_t, FontGlyph>::Iterator E = fd->glyph_map.find(p_glyph); + if (E) { + r_glyph = E->value; + return E->value.found; } if (glyph_index == 0) { // Non graphical or invalid glyph, do not render. - fd->glyph_map[p_glyph] = FontGlyph(); + E = fd->glyph_map.insert(p_glyph, FontGlyph()); + r_glyph = E->value; return true; } @@ -673,7 +676,8 @@ _FORCE_INLINE_ bool TextServerFallback::_ensure_glyph(FontFallback *p_font_data, int error = FT_Load_Glyph(fd->face, glyph_index, flags); if (error) { - fd->glyph_map[p_glyph] = FontGlyph(); + E = fd->glyph_map.insert(p_glyph, FontGlyph()); + r_glyph = E->value; return false; } @@ -777,20 +781,26 @@ _FORCE_INLINE_ bool TextServerFallback::_ensure_glyph(FontFallback *p_font_data, cleanup_stroker: FT_Stroker_Done(stroker); } - fd->glyph_map[p_glyph] = gl; + E = fd->glyph_map.insert(p_glyph, gl); + r_glyph = E->value; return gl.found; } #endif - fd->glyph_map[p_glyph] = FontGlyph(); + E = fd->glyph_map.insert(p_glyph, FontGlyph()); + r_glyph = E->value; return false; } -_FORCE_INLINE_ bool TextServerFallback::_ensure_cache_for_size(FontFallback *p_font_data, const Vector2i &p_size) const { +_FORCE_INLINE_ bool TextServerFallback::_ensure_cache_for_size(FontFallback *p_font_data, const Vector2i &p_size, FontForSizeFallback *&r_cache_for_size) const { ERR_FAIL_COND_V(p_size.x <= 0, false); - if (p_font_data->cache.has(p_size)) { + + HashMap<Vector2i, FontForSizeFallback *>::Iterator E = p_font_data->cache.find(p_size); + if (E) { + r_cache_for_size = E->value; return true; } + r_cache_for_size = nullptr; FontForSizeFallback *fd = memnew(FontForSizeFallback); fd->size = p_size; if (p_font_data->data_ptr && (p_font_data->data_size > 0)) { @@ -973,7 +983,9 @@ _FORCE_INLINE_ bool TextServerFallback::_ensure_cache_for_size(FontFallback *p_f ERR_FAIL_V_MSG(false, "FreeType: Can't load dynamic font, engine is compiled without FreeType support!"); #endif } - p_font_data->cache[p_size] = fd; + + p_font_data->cache.insert(p_size, fd); + r_cache_for_size = fd; return true; } @@ -1041,7 +1053,8 @@ void TextServerFallback::_font_set_style(const RID &p_font_rid, BitField<FontSty MutexLock lock(fd->mutex); Vector2i size = _get_size(fd, 16); - ERR_FAIL_COND(!_ensure_cache_for_size(fd, size)); + FontForSizeFallback *ffsd = nullptr; + ERR_FAIL_COND(!_ensure_cache_for_size(fd, size, ffsd)); fd->style_flags = p_style; } @@ -1119,7 +1132,8 @@ BitField<TextServer::FontStyle> TextServerFallback::_font_get_style(const RID &p MutexLock lock(fd->mutex); Vector2i size = _get_size(fd, 16); - ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size), 0); + FontForSizeFallback *ffsd = nullptr; + ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size, ffsd), 0); return fd->style_flags; } @@ -1129,7 +1143,8 @@ void TextServerFallback::_font_set_style_name(const RID &p_font_rid, const Strin MutexLock lock(fd->mutex); Vector2i size = _get_size(fd, 16); - ERR_FAIL_COND(!_ensure_cache_for_size(fd, size)); + FontForSizeFallback *ffsd = nullptr; + ERR_FAIL_COND(!_ensure_cache_for_size(fd, size, ffsd)); fd->style_name = p_name; } @@ -1139,7 +1154,8 @@ String TextServerFallback::_font_get_style_name(const RID &p_font_rid) const { MutexLock lock(fd->mutex); Vector2i size = _get_size(fd, 16); - ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size), String()); + FontForSizeFallback *ffsd = nullptr; + ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size, ffsd), String()); return fd->style_name; } @@ -1149,7 +1165,8 @@ void TextServerFallback::_font_set_weight(const RID &p_font_rid, int64_t p_weigh MutexLock lock(fd->mutex); Vector2i size = _get_size(fd, 16); - ERR_FAIL_COND(!_ensure_cache_for_size(fd, size)); + FontForSizeFallback *ffsd = nullptr; + ERR_FAIL_COND(!_ensure_cache_for_size(fd, size, ffsd)); fd->weight = CLAMP(p_weight, 100, 999); } @@ -1159,7 +1176,8 @@ int64_t TextServerFallback::_font_get_weight(const RID &p_font_rid) const { MutexLock lock(fd->mutex); Vector2i size = _get_size(fd, 16); - ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size), 400); + FontForSizeFallback *ffsd = nullptr; + ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size, ffsd), 400); return fd->weight; } @@ -1169,7 +1187,8 @@ void TextServerFallback::_font_set_stretch(const RID &p_font_rid, int64_t p_stre MutexLock lock(fd->mutex); Vector2i size = _get_size(fd, 16); - ERR_FAIL_COND(!_ensure_cache_for_size(fd, size)); + FontForSizeFallback *ffsd = nullptr; + ERR_FAIL_COND(!_ensure_cache_for_size(fd, size, ffsd)); fd->stretch = CLAMP(p_stretch, 50, 200); } @@ -1179,7 +1198,8 @@ int64_t TextServerFallback::_font_get_stretch(const RID &p_font_rid) const { MutexLock lock(fd->mutex); Vector2i size = _get_size(fd, 16); - ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size), 100); + FontForSizeFallback *ffsd = nullptr; + ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size, ffsd), 100); return fd->stretch; } @@ -1189,7 +1209,8 @@ void TextServerFallback::_font_set_name(const RID &p_font_rid, const String &p_n MutexLock lock(fd->mutex); Vector2i size = _get_size(fd, 16); - ERR_FAIL_COND(!_ensure_cache_for_size(fd, size)); + FontForSizeFallback *ffsd = nullptr; + ERR_FAIL_COND(!_ensure_cache_for_size(fd, size, ffsd)); fd->font_name = p_name; } @@ -1199,7 +1220,8 @@ String TextServerFallback::_font_get_name(const RID &p_font_rid) const { MutexLock lock(fd->mutex); Vector2i size = _get_size(fd, 16); - ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size), String()); + FontForSizeFallback *ffsd = nullptr; + ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size, ffsd), String()); return fd->font_name; } @@ -1607,8 +1629,9 @@ void TextServerFallback::_font_set_ascent(const RID &p_font_rid, int64_t p_size, MutexLock lock(fd->mutex); Vector2i size = _get_size(fd, p_size); - ERR_FAIL_COND(!_ensure_cache_for_size(fd, size)); - fd->cache[size]->ascent = p_ascent; + FontForSizeFallback *ffsd = nullptr; + ERR_FAIL_COND(!_ensure_cache_for_size(fd, size, ffsd)); + ffsd->ascent = p_ascent; } double TextServerFallback::_font_get_ascent(const RID &p_font_rid, int64_t p_size) const { @@ -1618,18 +1641,19 @@ double TextServerFallback::_font_get_ascent(const RID &p_font_rid, int64_t p_siz MutexLock lock(fd->mutex); Vector2i size = _get_size(fd, p_size); - ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size), 0.0); + FontForSizeFallback *ffsd = nullptr; + ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size, ffsd), 0.0); if (fd->msdf) { - return fd->cache[size]->ascent * (double)p_size / (double)fd->msdf_source_size; + return ffsd->ascent * (double)p_size / (double)fd->msdf_source_size; } else if (fd->fixed_size > 0 && fd->fixed_size_scale_mode != FIXED_SIZE_SCALE_DISABLE && size.x != p_size) { if (fd->fixed_size_scale_mode == FIXED_SIZE_SCALE_ENABLED) { - return fd->cache[size]->ascent * (double)p_size / (double)fd->fixed_size; + return ffsd->ascent * (double)p_size / (double)fd->fixed_size; } else { - return fd->cache[size]->ascent * Math::round((double)p_size / (double)fd->fixed_size); + return ffsd->ascent * Math::round((double)p_size / (double)fd->fixed_size); } } else { - return fd->cache[size]->ascent; + return ffsd->ascent; } } @@ -1639,8 +1663,9 @@ void TextServerFallback::_font_set_descent(const RID &p_font_rid, int64_t p_size Vector2i size = _get_size(fd, p_size); - ERR_FAIL_COND(!_ensure_cache_for_size(fd, size)); - fd->cache[size]->descent = p_descent; + FontForSizeFallback *ffsd = nullptr; + ERR_FAIL_COND(!_ensure_cache_for_size(fd, size, ffsd)); + ffsd->descent = p_descent; } double TextServerFallback::_font_get_descent(const RID &p_font_rid, int64_t p_size) const { @@ -1650,18 +1675,19 @@ double TextServerFallback::_font_get_descent(const RID &p_font_rid, int64_t p_si MutexLock lock(fd->mutex); Vector2i size = _get_size(fd, p_size); - ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size), 0.0); + FontForSizeFallback *ffsd = nullptr; + ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size, ffsd), 0.0); if (fd->msdf) { - return fd->cache[size]->descent * (double)p_size / (double)fd->msdf_source_size; + return ffsd->descent * (double)p_size / (double)fd->msdf_source_size; } else if (fd->fixed_size > 0 && fd->fixed_size_scale_mode != FIXED_SIZE_SCALE_DISABLE && size.x != p_size) { if (fd->fixed_size_scale_mode == FIXED_SIZE_SCALE_ENABLED) { - return fd->cache[size]->descent * (double)p_size / (double)fd->fixed_size; + return ffsd->descent * (double)p_size / (double)fd->fixed_size; } else { - return fd->cache[size]->descent * Math::round((double)p_size / (double)fd->fixed_size); + return ffsd->descent * Math::round((double)p_size / (double)fd->fixed_size); } } else { - return fd->cache[size]->descent; + return ffsd->descent; } } @@ -1672,8 +1698,9 @@ void TextServerFallback::_font_set_underline_position(const RID &p_font_rid, int MutexLock lock(fd->mutex); Vector2i size = _get_size(fd, p_size); - ERR_FAIL_COND(!_ensure_cache_for_size(fd, size)); - fd->cache[size]->underline_position = p_underline_position; + FontForSizeFallback *ffsd = nullptr; + ERR_FAIL_COND(!_ensure_cache_for_size(fd, size, ffsd)); + ffsd->underline_position = p_underline_position; } double TextServerFallback::_font_get_underline_position(const RID &p_font_rid, int64_t p_size) const { @@ -1683,18 +1710,19 @@ double TextServerFallback::_font_get_underline_position(const RID &p_font_rid, i MutexLock lock(fd->mutex); Vector2i size = _get_size(fd, p_size); - ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size), 0.0); + FontForSizeFallback *ffsd = nullptr; + ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size, ffsd), 0.0); if (fd->msdf) { - return fd->cache[size]->underline_position * (double)p_size / (double)fd->msdf_source_size; + return ffsd->underline_position * (double)p_size / (double)fd->msdf_source_size; } else if (fd->fixed_size > 0 && fd->fixed_size_scale_mode != FIXED_SIZE_SCALE_DISABLE && size.x != p_size) { if (fd->fixed_size_scale_mode == FIXED_SIZE_SCALE_ENABLED) { - return fd->cache[size]->underline_position * (double)p_size / (double)fd->fixed_size; + return ffsd->underline_position * (double)p_size / (double)fd->fixed_size; } else { - return fd->cache[size]->underline_position * Math::round((double)p_size / (double)fd->fixed_size); + return ffsd->underline_position * Math::round((double)p_size / (double)fd->fixed_size); } } else { - return fd->cache[size]->underline_position; + return ffsd->underline_position; } } @@ -1705,8 +1733,9 @@ void TextServerFallback::_font_set_underline_thickness(const RID &p_font_rid, in MutexLock lock(fd->mutex); Vector2i size = _get_size(fd, p_size); - ERR_FAIL_COND(!_ensure_cache_for_size(fd, size)); - fd->cache[size]->underline_thickness = p_underline_thickness; + FontForSizeFallback *ffsd = nullptr; + ERR_FAIL_COND(!_ensure_cache_for_size(fd, size, ffsd)); + ffsd->underline_thickness = p_underline_thickness; } double TextServerFallback::_font_get_underline_thickness(const RID &p_font_rid, int64_t p_size) const { @@ -1716,18 +1745,19 @@ double TextServerFallback::_font_get_underline_thickness(const RID &p_font_rid, MutexLock lock(fd->mutex); Vector2i size = _get_size(fd, p_size); - ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size), 0.0); + FontForSizeFallback *ffsd = nullptr; + ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size, ffsd), 0.0); if (fd->msdf) { - return fd->cache[size]->underline_thickness * (double)p_size / (double)fd->msdf_source_size; + return ffsd->underline_thickness * (double)p_size / (double)fd->msdf_source_size; } else if (fd->fixed_size > 0 && fd->fixed_size_scale_mode != FIXED_SIZE_SCALE_DISABLE && size.x != p_size) { if (fd->fixed_size_scale_mode == FIXED_SIZE_SCALE_ENABLED) { - return fd->cache[size]->underline_thickness * (double)p_size / (double)fd->fixed_size; + return ffsd->underline_thickness * (double)p_size / (double)fd->fixed_size; } else { - return fd->cache[size]->underline_thickness * Math::round((double)p_size / (double)fd->fixed_size); + return ffsd->underline_thickness * Math::round((double)p_size / (double)fd->fixed_size); } } else { - return fd->cache[size]->underline_thickness; + return ffsd->underline_thickness; } } @@ -1738,13 +1768,14 @@ void TextServerFallback::_font_set_scale(const RID &p_font_rid, int64_t p_size, MutexLock lock(fd->mutex); Vector2i size = _get_size(fd, p_size); - ERR_FAIL_COND(!_ensure_cache_for_size(fd, size)); + FontForSizeFallback *ffsd = nullptr; + ERR_FAIL_COND(!_ensure_cache_for_size(fd, size, ffsd)); #ifdef MODULE_FREETYPE_ENABLED - if (fd->cache[size]->face) { + if (ffsd->face) { return; // Do not override scale for dynamic fonts, it's calculated automatically. } #endif - fd->cache[size]->scale = p_scale; + ffsd->scale = p_scale; } double TextServerFallback::_font_get_scale(const RID &p_font_rid, int64_t p_size) const { @@ -1754,18 +1785,19 @@ double TextServerFallback::_font_get_scale(const RID &p_font_rid, int64_t p_size MutexLock lock(fd->mutex); Vector2i size = _get_size(fd, p_size); - ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size), 0.0); + FontForSizeFallback *ffsd = nullptr; + ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size, ffsd), 0.0); if (fd->msdf) { - return fd->cache[size]->scale * (double)p_size / (double)fd->msdf_source_size; + return ffsd->scale * (double)p_size / (double)fd->msdf_source_size; } else if (fd->fixed_size > 0 && fd->fixed_size_scale_mode != FIXED_SIZE_SCALE_DISABLE && size.x != p_size) { if (fd->fixed_size_scale_mode == FIXED_SIZE_SCALE_ENABLED) { - return fd->cache[size]->scale * (double)p_size / (double)fd->fixed_size; + return ffsd->scale * (double)p_size / (double)fd->fixed_size; } else { - return fd->cache[size]->scale * Math::round((double)p_size / (double)fd->fixed_size); + return ffsd->scale * Math::round((double)p_size / (double)fd->fixed_size); } } else { - return fd->cache[size]->scale / fd->cache[size]->oversampling; + return ffsd->scale / ffsd->oversampling; } } @@ -1776,9 +1808,10 @@ int64_t TextServerFallback::_font_get_texture_count(const RID &p_font_rid, const MutexLock lock(fd->mutex); Vector2i size = _get_size_outline(fd, p_size); - ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size), 0); + FontForSizeFallback *ffsd = nullptr; + ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size, ffsd), 0); - return fd->cache[size]->textures.size(); + return ffsd->textures.size(); } void TextServerFallback::_font_clear_textures(const RID &p_font_rid, const Vector2i &p_size) { @@ -1787,8 +1820,9 @@ void TextServerFallback::_font_clear_textures(const RID &p_font_rid, const Vecto MutexLock lock(fd->mutex); Vector2i size = _get_size_outline(fd, p_size); - ERR_FAIL_COND(!_ensure_cache_for_size(fd, size)); - fd->cache[size]->textures.clear(); + FontForSizeFallback *ffsd = nullptr; + ERR_FAIL_COND(!_ensure_cache_for_size(fd, size, ffsd)); + ffsd->textures.clear(); } void TextServerFallback::_font_remove_texture(const RID &p_font_rid, const Vector2i &p_size, int64_t p_texture_index) { @@ -1797,10 +1831,11 @@ void TextServerFallback::_font_remove_texture(const RID &p_font_rid, const Vecto MutexLock lock(fd->mutex); Vector2i size = _get_size_outline(fd, p_size); - ERR_FAIL_COND(!_ensure_cache_for_size(fd, size)); - ERR_FAIL_INDEX(p_texture_index, fd->cache[size]->textures.size()); + FontForSizeFallback *ffsd = nullptr; + ERR_FAIL_COND(!_ensure_cache_for_size(fd, size, ffsd)); + ERR_FAIL_INDEX(p_texture_index, ffsd->textures.size()); - fd->cache[size]->textures.remove_at(p_texture_index); + ffsd->textures.remove_at(p_texture_index); } void TextServerFallback::_font_set_texture_image(const RID &p_font_rid, const Vector2i &p_size, int64_t p_texture_index, const Ref<Image> &p_image) { @@ -1810,13 +1845,14 @@ void TextServerFallback::_font_set_texture_image(const RID &p_font_rid, const Ve MutexLock lock(fd->mutex); Vector2i size = _get_size_outline(fd, p_size); - ERR_FAIL_COND(!_ensure_cache_for_size(fd, size)); + FontForSizeFallback *ffsd = nullptr; + ERR_FAIL_COND(!_ensure_cache_for_size(fd, size, ffsd)); ERR_FAIL_COND(p_texture_index < 0); - if (p_texture_index >= fd->cache[size]->textures.size()) { - fd->cache[size]->textures.resize(p_texture_index + 1); + if (p_texture_index >= ffsd->textures.size()) { + ffsd->textures.resize(p_texture_index + 1); } - ShelfPackTexture &tex = fd->cache[size]->textures.write[p_texture_index]; + ShelfPackTexture &tex = ffsd->textures.write[p_texture_index]; tex.image = p_image; tex.texture_w = p_image->get_width(); @@ -1837,10 +1873,11 @@ Ref<Image> TextServerFallback::_font_get_texture_image(const RID &p_font_rid, co MutexLock lock(fd->mutex); Vector2i size = _get_size_outline(fd, p_size); - ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size), Ref<Image>()); - ERR_FAIL_INDEX_V(p_texture_index, fd->cache[size]->textures.size(), Ref<Image>()); + FontForSizeFallback *ffsd = nullptr; + ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size, ffsd), Ref<Image>()); + ERR_FAIL_INDEX_V(p_texture_index, ffsd->textures.size(), Ref<Image>()); - const ShelfPackTexture &tex = fd->cache[size]->textures[p_texture_index]; + const ShelfPackTexture &tex = ffsd->textures[p_texture_index]; return tex.image; } @@ -1851,13 +1888,14 @@ void TextServerFallback::_font_set_texture_offsets(const RID &p_font_rid, const MutexLock lock(fd->mutex); Vector2i size = _get_size_outline(fd, p_size); - ERR_FAIL_COND(!_ensure_cache_for_size(fd, size)); + FontForSizeFallback *ffsd = nullptr; + ERR_FAIL_COND(!_ensure_cache_for_size(fd, size, ffsd)); ERR_FAIL_COND(p_texture_index < 0); - if (p_texture_index >= fd->cache[size]->textures.size()) { - fd->cache[size]->textures.resize(p_texture_index + 1); + if (p_texture_index >= ffsd->textures.size()) { + ffsd->textures.resize(p_texture_index + 1); } - ShelfPackTexture &tex = fd->cache[size]->textures.write[p_texture_index]; + ShelfPackTexture &tex = ffsd->textures.write[p_texture_index]; tex.shelves.clear(); for (int32_t i = 0; i < p_offsets.size(); i += 4) { tex.shelves.push_back(Shelf(p_offsets[i], p_offsets[i + 1], p_offsets[i + 2], p_offsets[i + 3])); @@ -1870,10 +1908,11 @@ PackedInt32Array TextServerFallback::_font_get_texture_offsets(const RID &p_font MutexLock lock(fd->mutex); Vector2i size = _get_size_outline(fd, p_size); - ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size), PackedInt32Array()); - ERR_FAIL_INDEX_V(p_texture_index, fd->cache[size]->textures.size(), PackedInt32Array()); + FontForSizeFallback *ffsd = nullptr; + ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size, ffsd), PackedInt32Array()); + ERR_FAIL_INDEX_V(p_texture_index, ffsd->textures.size(), PackedInt32Array()); - const ShelfPackTexture &tex = fd->cache[size]->textures[p_texture_index]; + const ShelfPackTexture &tex = ffsd->textures[p_texture_index]; PackedInt32Array ret; ret.resize(tex.shelves.size() * 4); @@ -1895,10 +1934,11 @@ PackedInt32Array TextServerFallback::_font_get_glyph_list(const RID &p_font_rid, MutexLock lock(fd->mutex); Vector2i size = _get_size_outline(fd, p_size); - ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size), PackedInt32Array()); + FontForSizeFallback *ffsd = nullptr; + ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size, ffsd), PackedInt32Array()); PackedInt32Array ret; - const HashMap<int32_t, FontGlyph> &gl = fd->cache[size]->glyph_map; + const HashMap<int32_t, FontGlyph> &gl = ffsd->glyph_map; for (const KeyValue<int32_t, FontGlyph> &E : gl) { ret.push_back(E.key); } @@ -1911,9 +1951,10 @@ void TextServerFallback::_font_clear_glyphs(const RID &p_font_rid, const Vector2 MutexLock lock(fd->mutex); Vector2i size = _get_size_outline(fd, p_size); - ERR_FAIL_COND(!_ensure_cache_for_size(fd, size)); + FontForSizeFallback *ffsd = nullptr; + ERR_FAIL_COND(!_ensure_cache_for_size(fd, size, ffsd)); - fd->cache[size]->glyph_map.clear(); + ffsd->glyph_map.clear(); } void TextServerFallback::_font_remove_glyph(const RID &p_font_rid, const Vector2i &p_size, int64_t p_glyph) { @@ -1922,9 +1963,10 @@ void TextServerFallback::_font_remove_glyph(const RID &p_font_rid, const Vector2 MutexLock lock(fd->mutex); Vector2i size = _get_size_outline(fd, p_size); - ERR_FAIL_COND(!_ensure_cache_for_size(fd, size)); + FontForSizeFallback *ffsd = nullptr; + ERR_FAIL_COND(!_ensure_cache_for_size(fd, size, ffsd)); - fd->cache[size]->glyph_map.erase(p_glyph); + ffsd->glyph_map.erase(p_glyph); } Vector2 TextServerFallback::_font_get_glyph_advance(const RID &p_font_rid, int64_t p_size, int64_t p_glyph) const { @@ -1934,22 +1976,22 @@ Vector2 TextServerFallback::_font_get_glyph_advance(const RID &p_font_rid, int64 MutexLock lock(fd->mutex); Vector2i size = _get_size(fd, p_size); - ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size), Vector2()); + FontForSizeFallback *ffsd = nullptr; + ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size, ffsd), Vector2()); int mod = 0; if (fd->antialiasing == FONT_ANTIALIASING_LCD) { - TextServer::FontLCDSubpixelLayout layout = (TextServer::FontLCDSubpixelLayout)(int)GLOBAL_GET("gui/theme/lcd_subpixel_layout"); + TextServer::FontLCDSubpixelLayout layout = lcd_subpixel_layout.get(); if (layout != FONT_LCD_SUBPIXEL_LAYOUT_NONE) { mod = (layout << 24); } } - if (!_ensure_glyph(fd, size, p_glyph | mod)) { + FontGlyph fgl; + if (!_ensure_glyph(fd, size, p_glyph | mod, fgl)) { return Vector2(); // Invalid or non graphicl glyph, do not display errors. } - const HashMap<int32_t, FontGlyph> &gl = fd->cache[size]->glyph_map; - Vector2 ea; if (fd->embolden != 0.0) { ea.x = fd->embolden * double(size.x) / 64.0; @@ -1957,17 +1999,17 @@ Vector2 TextServerFallback::_font_get_glyph_advance(const RID &p_font_rid, int64 double scale = _font_get_scale(p_font_rid, p_size); if (fd->msdf) { - return (gl[p_glyph | mod].advance + ea) * (double)p_size / (double)fd->msdf_source_size; + return (fgl.advance + ea) * (double)p_size / (double)fd->msdf_source_size; } else if (fd->fixed_size > 0 && fd->fixed_size_scale_mode != FIXED_SIZE_SCALE_DISABLE && size.x != p_size) { if (fd->fixed_size_scale_mode == FIXED_SIZE_SCALE_ENABLED) { - return (gl[p_glyph | mod].advance + ea) * (double)p_size / (double)fd->fixed_size; + return (fgl.advance + ea) * (double)p_size / (double)fd->fixed_size; } else { - return (gl[p_glyph | mod].advance + ea) * Math::round((double)p_size / (double)fd->fixed_size); + return (fgl.advance + ea) * Math::round((double)p_size / (double)fd->fixed_size); } } else if ((scale == 1.0) && ((fd->subpixel_positioning == SUBPIXEL_POSITIONING_DISABLED) || (fd->subpixel_positioning == SUBPIXEL_POSITIONING_AUTO && size.x > SUBPIXEL_POSITIONING_ONE_HALF_MAX_SIZE))) { - return (gl[p_glyph | mod].advance + ea).round(); + return (fgl.advance + ea).round(); } else { - return gl[p_glyph | mod].advance + ea; + return fgl.advance + ea; } } @@ -1978,12 +2020,13 @@ void TextServerFallback::_font_set_glyph_advance(const RID &p_font_rid, int64_t MutexLock lock(fd->mutex); Vector2i size = _get_size(fd, p_size); - ERR_FAIL_COND(!_ensure_cache_for_size(fd, size)); + FontForSizeFallback *ffsd = nullptr; + ERR_FAIL_COND(!_ensure_cache_for_size(fd, size, ffsd)); - HashMap<int32_t, FontGlyph> &gl = fd->cache[size]->glyph_map; + FontGlyph &fgl = ffsd->glyph_map[p_glyph]; - gl[p_glyph].advance = p_advance; - gl[p_glyph].found = true; + fgl.advance = p_advance; + fgl.found = true; } Vector2 TextServerFallback::_font_get_glyph_offset(const RID &p_font_rid, const Vector2i &p_size, int64_t p_glyph) const { @@ -1993,32 +2036,32 @@ Vector2 TextServerFallback::_font_get_glyph_offset(const RID &p_font_rid, const MutexLock lock(fd->mutex); Vector2i size = _get_size_outline(fd, p_size); - ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size), Vector2()); + FontForSizeFallback *ffsd = nullptr; + ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size, ffsd), Vector2()); int mod = 0; if (fd->antialiasing == FONT_ANTIALIASING_LCD) { - TextServer::FontLCDSubpixelLayout layout = (TextServer::FontLCDSubpixelLayout)(int)GLOBAL_GET("gui/theme/lcd_subpixel_layout"); + TextServer::FontLCDSubpixelLayout layout = lcd_subpixel_layout.get(); if (layout != FONT_LCD_SUBPIXEL_LAYOUT_NONE) { mod = (layout << 24); } } - if (!_ensure_glyph(fd, size, p_glyph | mod)) { + FontGlyph fgl; + if (!_ensure_glyph(fd, size, p_glyph | mod, fgl)) { return Vector2(); // Invalid or non graphicl glyph, do not display errors. } - const HashMap<int32_t, FontGlyph> &gl = fd->cache[size]->glyph_map; - if (fd->msdf) { - return gl[p_glyph | mod].rect.position * (double)p_size.x / (double)fd->msdf_source_size; + return fgl.rect.position * (double)p_size.x / (double)fd->msdf_source_size; } else if (fd->fixed_size > 0 && fd->fixed_size_scale_mode != FIXED_SIZE_SCALE_DISABLE && size.x != p_size.x) { if (fd->fixed_size_scale_mode == FIXED_SIZE_SCALE_ENABLED) { - return gl[p_glyph | mod].rect.position * (double)p_size.x / (double)fd->fixed_size; + return fgl.rect.position * (double)p_size.x / (double)fd->fixed_size; } else { - return gl[p_glyph | mod].rect.position * Math::round((double)p_size.x / (double)fd->fixed_size); + return fgl.rect.position * Math::round((double)p_size.x / (double)fd->fixed_size); } } else { - return gl[p_glyph | mod].rect.position; + return fgl.rect.position; } } @@ -2029,12 +2072,13 @@ void TextServerFallback::_font_set_glyph_offset(const RID &p_font_rid, const Vec MutexLock lock(fd->mutex); Vector2i size = _get_size_outline(fd, p_size); - ERR_FAIL_COND(!_ensure_cache_for_size(fd, size)); + FontForSizeFallback *ffsd = nullptr; + ERR_FAIL_COND(!_ensure_cache_for_size(fd, size, ffsd)); - HashMap<int32_t, FontGlyph> &gl = fd->cache[size]->glyph_map; + FontGlyph &fgl = ffsd->glyph_map[p_glyph]; - gl[p_glyph].rect.position = p_offset; - gl[p_glyph].found = true; + fgl.rect.position = p_offset; + fgl.found = true; } Vector2 TextServerFallback::_font_get_glyph_size(const RID &p_font_rid, const Vector2i &p_size, int64_t p_glyph) const { @@ -2044,32 +2088,32 @@ Vector2 TextServerFallback::_font_get_glyph_size(const RID &p_font_rid, const Ve MutexLock lock(fd->mutex); Vector2i size = _get_size_outline(fd, p_size); - ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size), Vector2()); + FontForSizeFallback *ffsd = nullptr; + ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size, ffsd), Vector2()); int mod = 0; if (fd->antialiasing == FONT_ANTIALIASING_LCD) { - TextServer::FontLCDSubpixelLayout layout = (TextServer::FontLCDSubpixelLayout)(int)GLOBAL_GET("gui/theme/lcd_subpixel_layout"); + TextServer::FontLCDSubpixelLayout layout = lcd_subpixel_layout.get(); if (layout != FONT_LCD_SUBPIXEL_LAYOUT_NONE) { mod = (layout << 24); } } - if (!_ensure_glyph(fd, size, p_glyph | mod)) { + FontGlyph fgl; + if (!_ensure_glyph(fd, size, p_glyph | mod, fgl)) { return Vector2(); // Invalid or non graphicl glyph, do not display errors. } - const HashMap<int32_t, FontGlyph> &gl = fd->cache[size]->glyph_map; - if (fd->msdf) { - return gl[p_glyph | mod].rect.size * (double)p_size.x / (double)fd->msdf_source_size; + return fgl.rect.size * (double)p_size.x / (double)fd->msdf_source_size; } else if (fd->fixed_size > 0 && fd->fixed_size_scale_mode != FIXED_SIZE_SCALE_DISABLE && size.x != p_size.x) { if (fd->fixed_size_scale_mode == FIXED_SIZE_SCALE_ENABLED) { - return gl[p_glyph | mod].rect.size * (double)p_size.x / (double)fd->fixed_size; + return fgl.rect.size * (double)p_size.x / (double)fd->fixed_size; } else { - return gl[p_glyph | mod].rect.size * Math::round((double)p_size.x / (double)fd->fixed_size); + return fgl.rect.size * Math::round((double)p_size.x / (double)fd->fixed_size); } } else { - return gl[p_glyph | mod].rect.size; + return fgl.rect.size; } } @@ -2080,12 +2124,13 @@ void TextServerFallback::_font_set_glyph_size(const RID &p_font_rid, const Vecto MutexLock lock(fd->mutex); Vector2i size = _get_size_outline(fd, p_size); - ERR_FAIL_COND(!_ensure_cache_for_size(fd, size)); + FontForSizeFallback *ffsd = nullptr; + ERR_FAIL_COND(!_ensure_cache_for_size(fd, size, ffsd)); - HashMap<int32_t, FontGlyph> &gl = fd->cache[size]->glyph_map; + FontGlyph &fgl = ffsd->glyph_map[p_glyph]; - gl[p_glyph].rect.size = p_gl_size; - gl[p_glyph].found = true; + fgl.rect.size = p_gl_size; + fgl.found = true; } Rect2 TextServerFallback::_font_get_glyph_uv_rect(const RID &p_font_rid, const Vector2i &p_size, int64_t p_glyph) const { @@ -2095,22 +2140,23 @@ Rect2 TextServerFallback::_font_get_glyph_uv_rect(const RID &p_font_rid, const V MutexLock lock(fd->mutex); Vector2i size = _get_size_outline(fd, p_size); - ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size), Rect2()); + FontForSizeFallback *ffsd = nullptr; + ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size, ffsd), Rect2()); int mod = 0; if (fd->antialiasing == FONT_ANTIALIASING_LCD) { - TextServer::FontLCDSubpixelLayout layout = (TextServer::FontLCDSubpixelLayout)(int)GLOBAL_GET("gui/theme/lcd_subpixel_layout"); + TextServer::FontLCDSubpixelLayout layout = lcd_subpixel_layout.get(); if (layout != FONT_LCD_SUBPIXEL_LAYOUT_NONE) { mod = (layout << 24); } } - if (!_ensure_glyph(fd, size, p_glyph | mod)) { + FontGlyph fgl; + if (!_ensure_glyph(fd, size, p_glyph | mod, fgl)) { return Rect2(); // Invalid or non graphicl glyph, do not display errors. } - const HashMap<int32_t, FontGlyph> &gl = fd->cache[size]->glyph_map; - return gl[p_glyph | mod].uv_rect; + return fgl.uv_rect; } void TextServerFallback::_font_set_glyph_uv_rect(const RID &p_font_rid, const Vector2i &p_size, int64_t p_glyph, const Rect2 &p_uv_rect) { @@ -2120,12 +2166,13 @@ void TextServerFallback::_font_set_glyph_uv_rect(const RID &p_font_rid, const Ve MutexLock lock(fd->mutex); Vector2i size = _get_size_outline(fd, p_size); - ERR_FAIL_COND(!_ensure_cache_for_size(fd, size)); + FontForSizeFallback *ffsd = nullptr; + ERR_FAIL_COND(!_ensure_cache_for_size(fd, size, ffsd)); - HashMap<int32_t, FontGlyph> &gl = fd->cache[size]->glyph_map; + FontGlyph &fgl = ffsd->glyph_map[p_glyph]; - gl[p_glyph].uv_rect = p_uv_rect; - gl[p_glyph].found = true; + fgl.uv_rect = p_uv_rect; + fgl.found = true; } int64_t TextServerFallback::_font_get_glyph_texture_idx(const RID &p_font_rid, const Vector2i &p_size, int64_t p_glyph) const { @@ -2135,22 +2182,23 @@ int64_t TextServerFallback::_font_get_glyph_texture_idx(const RID &p_font_rid, c MutexLock lock(fd->mutex); Vector2i size = _get_size_outline(fd, p_size); - ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size), -1); + FontForSizeFallback *ffsd = nullptr; + ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size, ffsd), -1); int mod = 0; if (fd->antialiasing == FONT_ANTIALIASING_LCD) { - TextServer::FontLCDSubpixelLayout layout = (TextServer::FontLCDSubpixelLayout)(int)GLOBAL_GET("gui/theme/lcd_subpixel_layout"); + TextServer::FontLCDSubpixelLayout layout = lcd_subpixel_layout.get(); if (layout != FONT_LCD_SUBPIXEL_LAYOUT_NONE) { mod = (layout << 24); } } - if (!_ensure_glyph(fd, size, p_glyph | mod)) { + FontGlyph fgl; + if (!_ensure_glyph(fd, size, p_glyph | mod, fgl)) { return -1; // Invalid or non graphicl glyph, do not display errors. } - const HashMap<int32_t, FontGlyph> &gl = fd->cache[size]->glyph_map; - return gl[p_glyph | mod].texture_idx; + return fgl.texture_idx; } void TextServerFallback::_font_set_glyph_texture_idx(const RID &p_font_rid, const Vector2i &p_size, int64_t p_glyph, int64_t p_texture_idx) { @@ -2160,12 +2208,13 @@ void TextServerFallback::_font_set_glyph_texture_idx(const RID &p_font_rid, cons MutexLock lock(fd->mutex); Vector2i size = _get_size_outline(fd, p_size); - ERR_FAIL_COND(!_ensure_cache_for_size(fd, size)); + FontForSizeFallback *ffsd = nullptr; + ERR_FAIL_COND(!_ensure_cache_for_size(fd, size, ffsd)); - HashMap<int32_t, FontGlyph> &gl = fd->cache[size]->glyph_map; + FontGlyph &fgl = ffsd->glyph_map[p_glyph]; - gl[p_glyph].texture_idx = p_texture_idx; - gl[p_glyph].found = true; + fgl.texture_idx = p_texture_idx; + fgl.found = true; } RID TextServerFallback::_font_get_glyph_texture_rid(const RID &p_font_rid, const Vector2i &p_size, int64_t p_glyph) const { @@ -2175,27 +2224,28 @@ RID TextServerFallback::_font_get_glyph_texture_rid(const RID &p_font_rid, const MutexLock lock(fd->mutex); Vector2i size = _get_size_outline(fd, p_size); - ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size), RID()); + FontForSizeFallback *ffsd = nullptr; + ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size, ffsd), RID()); int mod = 0; if (fd->antialiasing == FONT_ANTIALIASING_LCD) { - TextServer::FontLCDSubpixelLayout layout = (TextServer::FontLCDSubpixelLayout)(int)GLOBAL_GET("gui/theme/lcd_subpixel_layout"); + TextServer::FontLCDSubpixelLayout layout = lcd_subpixel_layout.get(); if (layout != FONT_LCD_SUBPIXEL_LAYOUT_NONE) { mod = (layout << 24); } } - if (!_ensure_glyph(fd, size, p_glyph | mod)) { + FontGlyph fgl; + if (!_ensure_glyph(fd, size, p_glyph | mod, fgl)) { return RID(); // Invalid or non graphicl glyph, do not display errors. } - const HashMap<int32_t, FontGlyph> &gl = fd->cache[size]->glyph_map; - ERR_FAIL_COND_V(gl[p_glyph | mod].texture_idx < -1 || gl[p_glyph | mod].texture_idx >= fd->cache[size]->textures.size(), RID()); + ERR_FAIL_COND_V(fgl.texture_idx < -1 || fgl.texture_idx >= ffsd->textures.size(), RID()); if (RenderingServer::get_singleton() != nullptr) { - if (gl[p_glyph | mod].texture_idx != -1) { - if (fd->cache[size]->textures[gl[p_glyph | mod].texture_idx].dirty) { - ShelfPackTexture &tex = fd->cache[size]->textures.write[gl[p_glyph | mod].texture_idx]; + if (fgl.texture_idx != -1) { + if (ffsd->textures[fgl.texture_idx].dirty) { + ShelfPackTexture &tex = ffsd->textures.write[fgl.texture_idx]; Ref<Image> img = tex.image; if (fd->mipmaps && !img->has_mipmaps()) { img = tex.image->duplicate(); @@ -2208,7 +2258,7 @@ RID TextServerFallback::_font_get_glyph_texture_rid(const RID &p_font_rid, const } tex.dirty = false; } - return fd->cache[size]->textures[gl[p_glyph | mod].texture_idx].texture->get_rid(); + return ffsd->textures[fgl.texture_idx].texture->get_rid(); } } @@ -2222,27 +2272,28 @@ Size2 TextServerFallback::_font_get_glyph_texture_size(const RID &p_font_rid, co MutexLock lock(fd->mutex); Vector2i size = _get_size_outline(fd, p_size); - ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size), Size2()); + FontForSizeFallback *ffsd = nullptr; + ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size, ffsd), Size2()); int mod = 0; if (fd->antialiasing == FONT_ANTIALIASING_LCD) { - TextServer::FontLCDSubpixelLayout layout = (TextServer::FontLCDSubpixelLayout)(int)GLOBAL_GET("gui/theme/lcd_subpixel_layout"); + TextServer::FontLCDSubpixelLayout layout = lcd_subpixel_layout.get(); if (layout != FONT_LCD_SUBPIXEL_LAYOUT_NONE) { mod = (layout << 24); } } - if (!_ensure_glyph(fd, size, p_glyph | mod)) { + FontGlyph fgl; + if (!_ensure_glyph(fd, size, p_glyph | mod, fgl)) { return Size2(); // Invalid or non graphicl glyph, do not display errors. } - const HashMap<int32_t, FontGlyph> &gl = fd->cache[size]->glyph_map; - ERR_FAIL_COND_V(gl[p_glyph | mod].texture_idx < -1 || gl[p_glyph | mod].texture_idx >= fd->cache[size]->textures.size(), Size2()); + ERR_FAIL_COND_V(fgl.texture_idx < -1 || fgl.texture_idx >= ffsd->textures.size(), Size2()); if (RenderingServer::get_singleton() != nullptr) { - if (gl[p_glyph | mod].texture_idx != -1) { - if (fd->cache[size]->textures[gl[p_glyph | mod].texture_idx].dirty) { - ShelfPackTexture &tex = fd->cache[size]->textures.write[gl[p_glyph | mod].texture_idx]; + if (fgl.texture_idx != -1) { + if (ffsd->textures[fgl.texture_idx].dirty) { + ShelfPackTexture &tex = ffsd->textures.write[fgl.texture_idx]; Ref<Image> img = tex.image; if (fd->mipmaps && !img->has_mipmaps()) { img = tex.image->duplicate(); @@ -2255,7 +2306,7 @@ Size2 TextServerFallback::_font_get_glyph_texture_size(const RID &p_font_rid, co } tex.dirty = false; } - return fd->cache[size]->textures[gl[p_glyph | mod].texture_idx].texture->get_size(); + return ffsd->textures[fgl.texture_idx].texture->get_size(); } } @@ -2269,7 +2320,8 @@ Dictionary TextServerFallback::_font_get_glyph_contours(const RID &p_font_rid, i MutexLock lock(fd->mutex); Vector2i size = _get_size(fd, p_size); - ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size), Dictionary()); + FontForSizeFallback *ffsd = nullptr; + ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size, ffsd), Dictionary()); #ifdef MODULE_FREETYPE_ENABLED PackedVector3Array points; @@ -2277,20 +2329,20 @@ Dictionary TextServerFallback::_font_get_glyph_contours(const RID &p_font_rid, i int32_t index = p_index & 0xffffff; // Remove subpixel shifts. - int error = FT_Load_Glyph(fd->cache[size]->face, FT_Get_Char_Index(fd->cache[size]->face, index), FT_LOAD_NO_BITMAP | (fd->force_autohinter ? FT_LOAD_FORCE_AUTOHINT : 0)); + int error = FT_Load_Glyph(ffsd->face, FT_Get_Char_Index(ffsd->face, index), FT_LOAD_NO_BITMAP | (fd->force_autohinter ? FT_LOAD_FORCE_AUTOHINT : 0)); ERR_FAIL_COND_V(error, Dictionary()); if (fd->embolden != 0.f) { FT_Pos strength = fd->embolden * p_size * 4; // 26.6 fractional units (1 / 64). - FT_Outline_Embolden(&fd->cache[size]->face->glyph->outline, strength); + FT_Outline_Embolden(&ffsd->face->glyph->outline, strength); } if (fd->transform != Transform2D()) { FT_Matrix mat = { FT_Fixed(fd->transform[0][0] * 65536), FT_Fixed(fd->transform[0][1] * 65536), FT_Fixed(fd->transform[1][0] * 65536), FT_Fixed(fd->transform[1][1] * 65536) }; // 16.16 fractional units (1 / 65536). - FT_Outline_Transform(&fd->cache[size]->face->glyph->outline, &mat); + FT_Outline_Transform(&ffsd->face->glyph->outline, &mat); } - double scale = (1.0 / 64.0) / fd->cache[size]->oversampling * fd->cache[size]->scale; + double scale = (1.0 / 64.0) / ffsd->oversampling * ffsd->scale; if (fd->msdf) { scale = scale * (double)p_size / (double)fd->msdf_source_size; } else if (fd->fixed_size > 0 && fd->fixed_size_scale_mode != FIXED_SIZE_SCALE_DISABLE && size.x != p_size) { @@ -2300,13 +2352,13 @@ Dictionary TextServerFallback::_font_get_glyph_contours(const RID &p_font_rid, i scale = scale * Math::round((double)p_size / (double)fd->fixed_size); } } - for (short i = 0; i < fd->cache[size]->face->glyph->outline.n_points; i++) { - points.push_back(Vector3(fd->cache[size]->face->glyph->outline.points[i].x * scale, -fd->cache[size]->face->glyph->outline.points[i].y * scale, FT_CURVE_TAG(fd->cache[size]->face->glyph->outline.tags[i]))); + for (short i = 0; i < ffsd->face->glyph->outline.n_points; i++) { + points.push_back(Vector3(ffsd->face->glyph->outline.points[i].x * scale, -ffsd->face->glyph->outline.points[i].y * scale, FT_CURVE_TAG(ffsd->face->glyph->outline.tags[i]))); } - for (short i = 0; i < fd->cache[size]->face->glyph->outline.n_contours; i++) { - contours.push_back(fd->cache[size]->face->glyph->outline.contours[i]); + for (short i = 0; i < ffsd->face->glyph->outline.n_contours; i++) { + contours.push_back(ffsd->face->glyph->outline.contours[i]); } - bool orientation = (FT_Outline_Get_Orientation(&fd->cache[size]->face->glyph->outline) == FT_ORIENTATION_FILL_RIGHT); + bool orientation = (FT_Outline_Get_Orientation(&ffsd->face->glyph->outline) == FT_ORIENTATION_FILL_RIGHT); Dictionary out; out["points"] = points; @@ -2325,10 +2377,11 @@ TypedArray<Vector2i> TextServerFallback::_font_get_kerning_list(const RID &p_fon MutexLock lock(fd->mutex); Vector2i size = _get_size(fd, p_size); - ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size), TypedArray<Vector2i>()); + FontForSizeFallback *ffsd = nullptr; + ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size, ffsd), TypedArray<Vector2i>()); TypedArray<Vector2i> ret; - for (const KeyValue<Vector2i, Vector2> &E : fd->cache[size]->kerning_map) { + for (const KeyValue<Vector2i, Vector2> &E : ffsd->kerning_map) { ret.push_back(E.key); } return ret; @@ -2341,8 +2394,9 @@ void TextServerFallback::_font_clear_kerning_map(const RID &p_font_rid, int64_t MutexLock lock(fd->mutex); Vector2i size = _get_size(fd, p_size); - ERR_FAIL_COND(!_ensure_cache_for_size(fd, size)); - fd->cache[size]->kerning_map.clear(); + FontForSizeFallback *ffsd = nullptr; + ERR_FAIL_COND(!_ensure_cache_for_size(fd, size, ffsd)); + ffsd->kerning_map.clear(); } void TextServerFallback::_font_remove_kerning(const RID &p_font_rid, int64_t p_size, const Vector2i &p_glyph_pair) { @@ -2352,8 +2406,9 @@ void TextServerFallback::_font_remove_kerning(const RID &p_font_rid, int64_t p_s MutexLock lock(fd->mutex); Vector2i size = _get_size(fd, p_size); - ERR_FAIL_COND(!_ensure_cache_for_size(fd, size)); - fd->cache[size]->kerning_map.erase(p_glyph_pair); + FontForSizeFallback *ffsd = nullptr; + ERR_FAIL_COND(!_ensure_cache_for_size(fd, size, ffsd)); + ffsd->kerning_map.erase(p_glyph_pair); } void TextServerFallback::_font_set_kerning(const RID &p_font_rid, int64_t p_size, const Vector2i &p_glyph_pair, const Vector2 &p_kerning) { @@ -2363,8 +2418,9 @@ void TextServerFallback::_font_set_kerning(const RID &p_font_rid, int64_t p_size MutexLock lock(fd->mutex); Vector2i size = _get_size(fd, p_size); - ERR_FAIL_COND(!_ensure_cache_for_size(fd, size)); - fd->cache[size]->kerning_map[p_glyph_pair] = p_kerning; + FontForSizeFallback *ffsd = nullptr; + ERR_FAIL_COND(!_ensure_cache_for_size(fd, size, ffsd)); + ffsd->kerning_map[p_glyph_pair] = p_kerning; } Vector2 TextServerFallback::_font_get_kerning(const RID &p_font_rid, int64_t p_size, const Vector2i &p_glyph_pair) const { @@ -2374,9 +2430,10 @@ Vector2 TextServerFallback::_font_get_kerning(const RID &p_font_rid, int64_t p_s MutexLock lock(fd->mutex); Vector2i size = _get_size(fd, p_size); - ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size), Vector2()); + FontForSizeFallback *ffsd = nullptr; + ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size, ffsd), Vector2()); - const HashMap<Vector2i, Vector2> &kern = fd->cache[size]->kerning_map; + const HashMap<Vector2i, Vector2> &kern = ffsd->kerning_map; if (kern.has(p_glyph_pair)) { if (fd->msdf) { @@ -2392,11 +2449,11 @@ Vector2 TextServerFallback::_font_get_kerning(const RID &p_font_rid, int64_t p_s } } else { #ifdef MODULE_FREETYPE_ENABLED - if (fd->cache[size]->face) { + if (ffsd->face) { FT_Vector delta; - int32_t glyph_a = FT_Get_Char_Index(fd->cache[size]->face, p_glyph_pair.x); - int32_t glyph_b = FT_Get_Char_Index(fd->cache[size]->face, p_glyph_pair.y); - FT_Get_Kerning(fd->cache[size]->face, glyph_a, glyph_b, FT_KERNING_DEFAULT, &delta); + int32_t glyph_a = FT_Get_Char_Index(ffsd->face, p_glyph_pair.x); + int32_t glyph_b = FT_Get_Char_Index(ffsd->face, p_glyph_pair.y); + FT_Get_Kerning(ffsd->face, glyph_a, glyph_b, FT_KERNING_DEFAULT, &delta); if (fd->msdf) { return Vector2(delta.x, delta.y) * (double)p_size / (double)fd->msdf_source_size; } else if (fd->fixed_size > 0 && fd->fixed_size_scale_mode != FIXED_SIZE_SCALE_DISABLE && size.x != p_size) { @@ -2431,17 +2488,19 @@ bool TextServerFallback::_font_has_char(const RID &p_font_rid, int64_t p_char) c } MutexLock lock(fd->mutex); + FontForSizeFallback *ffsd = nullptr; if (fd->cache.is_empty()) { - ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, fd->msdf ? Vector2i(fd->msdf_source_size, 0) : Vector2i(16, 0)), false); + ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, fd->msdf ? Vector2i(fd->msdf_source_size, 0) : Vector2i(16, 0), ffsd), false); + } else { + ffsd = fd->cache.begin()->value; } - FontForSizeFallback *at_size = fd->cache.begin()->value; #ifdef MODULE_FREETYPE_ENABLED - if (at_size && at_size->face) { - return FT_Get_Char_Index(at_size->face, p_char) != 0; + if (ffsd->face) { + return FT_Get_Char_Index(ffsd->face, p_char) != 0; } #endif - return (at_size) ? at_size->glyph_map.has((int32_t)p_char) : false; + return ffsd->glyph_map.has((int32_t)p_char); } String TextServerFallback::_font_get_supported_chars(const RID &p_font_rid) const { @@ -2449,30 +2508,30 @@ String TextServerFallback::_font_get_supported_chars(const RID &p_font_rid) cons ERR_FAIL_NULL_V(fd, String()); MutexLock lock(fd->mutex); + FontForSizeFallback *ffsd = nullptr; if (fd->cache.is_empty()) { - ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, fd->msdf ? Vector2i(fd->msdf_source_size, 0) : Vector2i(16, 0)), String()); + ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, fd->msdf ? Vector2i(fd->msdf_source_size, 0) : Vector2i(16, 0), ffsd), String()); + } else { + ffsd = fd->cache.begin()->value; } - FontForSizeFallback *at_size = fd->cache.begin()->value; String chars; #ifdef MODULE_FREETYPE_ENABLED - if (at_size && at_size->face) { + if (ffsd->face) { FT_UInt gindex; - FT_ULong charcode = FT_Get_First_Char(at_size->face, &gindex); + FT_ULong charcode = FT_Get_First_Char(ffsd->face, &gindex); while (gindex != 0) { if (charcode != 0) { chars = chars + String::chr(charcode); } - charcode = FT_Get_Next_Char(at_size->face, charcode, &gindex); + charcode = FT_Get_Next_Char(ffsd->face, charcode, &gindex); } return chars; } #endif - if (at_size) { - const HashMap<int32_t, FontGlyph> &gl = at_size->glyph_map; - for (const KeyValue<int32_t, FontGlyph> &E : gl) { - chars = chars + String::chr(E.key); - } + const HashMap<int32_t, FontGlyph> &gl = ffsd->glyph_map; + for (const KeyValue<int32_t, FontGlyph> &E : gl) { + chars = chars + String::chr(E.key); } return chars; } @@ -2482,10 +2541,12 @@ PackedInt32Array TextServerFallback::_font_get_supported_glyphs(const RID &p_fon ERR_FAIL_NULL_V(fd, PackedInt32Array()); MutexLock lock(fd->mutex); + FontForSizeFallback *at_size = nullptr; if (fd->cache.is_empty()) { - ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, fd->msdf ? Vector2i(fd->msdf_source_size, 0) : Vector2i(16, 0)), PackedInt32Array()); + ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, fd->msdf ? Vector2i(fd->msdf_source_size, 0) : Vector2i(16, 0), at_size), PackedInt32Array()); + } else { + at_size = fd->cache.begin()->value; } - FontForSizeFallback *at_size = fd->cache.begin()->value; PackedInt32Array glyphs; #ifdef MODULE_FREETYPE_ENABLED @@ -2516,25 +2577,27 @@ void TextServerFallback::_font_render_range(const RID &p_font_rid, const Vector2 MutexLock lock(fd->mutex); Vector2i size = _get_size_outline(fd, p_size); - ERR_FAIL_COND(!_ensure_cache_for_size(fd, size)); + FontForSizeFallback *ffsd = nullptr; + ERR_FAIL_COND(!_ensure_cache_for_size(fd, size, ffsd)); for (int64_t i = p_start; i <= p_end; i++) { #ifdef MODULE_FREETYPE_ENABLED int32_t idx = i; - if (fd->cache[size]->face) { + if (ffsd->face) { + FontGlyph fgl; if (fd->msdf) { - _ensure_glyph(fd, size, (int32_t)idx); + _ensure_glyph(fd, size, (int32_t)idx, fgl); } else { for (int aa = 0; aa < ((fd->antialiasing == FONT_ANTIALIASING_LCD) ? FONT_LCD_SUBPIXEL_LAYOUT_MAX : 1); aa++) { if ((fd->subpixel_positioning == SUBPIXEL_POSITIONING_ONE_QUARTER) || (fd->subpixel_positioning == SUBPIXEL_POSITIONING_AUTO && size.x <= SUBPIXEL_POSITIONING_ONE_QUARTER_MAX_SIZE)) { - _ensure_glyph(fd, size, (int32_t)idx | (0 << 27) | (aa << 24)); - _ensure_glyph(fd, size, (int32_t)idx | (1 << 27) | (aa << 24)); - _ensure_glyph(fd, size, (int32_t)idx | (2 << 27) | (aa << 24)); - _ensure_glyph(fd, size, (int32_t)idx | (3 << 27) | (aa << 24)); + _ensure_glyph(fd, size, (int32_t)idx | (0 << 27) | (aa << 24), fgl); + _ensure_glyph(fd, size, (int32_t)idx | (1 << 27) | (aa << 24), fgl); + _ensure_glyph(fd, size, (int32_t)idx | (2 << 27) | (aa << 24), fgl); + _ensure_glyph(fd, size, (int32_t)idx | (3 << 27) | (aa << 24), fgl); } else if ((fd->subpixel_positioning == SUBPIXEL_POSITIONING_ONE_HALF) || (fd->subpixel_positioning == SUBPIXEL_POSITIONING_AUTO && size.x <= SUBPIXEL_POSITIONING_ONE_HALF_MAX_SIZE)) { - _ensure_glyph(fd, size, (int32_t)idx | (1 << 27) | (aa << 24)); - _ensure_glyph(fd, size, (int32_t)idx | (0 << 27) | (aa << 24)); + _ensure_glyph(fd, size, (int32_t)idx | (1 << 27) | (aa << 24), fgl); + _ensure_glyph(fd, size, (int32_t)idx | (0 << 27) | (aa << 24), fgl); } else { - _ensure_glyph(fd, size, (int32_t)idx | (aa << 24)); + _ensure_glyph(fd, size, (int32_t)idx | (aa << 24), fgl); } } } @@ -2549,24 +2612,26 @@ void TextServerFallback::_font_render_glyph(const RID &p_font_rid, const Vector2 MutexLock lock(fd->mutex); Vector2i size = _get_size_outline(fd, p_size); - ERR_FAIL_COND(!_ensure_cache_for_size(fd, size)); + FontForSizeFallback *ffsd = nullptr; + ERR_FAIL_COND(!_ensure_cache_for_size(fd, size, ffsd)); #ifdef MODULE_FREETYPE_ENABLED int32_t idx = p_index & 0xffffff; // Remove subpixel shifts. - if (fd->cache[size]->face) { + if (ffsd->face) { + FontGlyph fgl; if (fd->msdf) { - _ensure_glyph(fd, size, (int32_t)idx); + _ensure_glyph(fd, size, (int32_t)idx, fgl); } else { for (int aa = 0; aa < ((fd->antialiasing == FONT_ANTIALIASING_LCD) ? FONT_LCD_SUBPIXEL_LAYOUT_MAX : 1); aa++) { if ((fd->subpixel_positioning == SUBPIXEL_POSITIONING_ONE_QUARTER) || (fd->subpixel_positioning == SUBPIXEL_POSITIONING_AUTO && size.x <= SUBPIXEL_POSITIONING_ONE_QUARTER_MAX_SIZE)) { - _ensure_glyph(fd, size, (int32_t)idx | (0 << 27) | (aa << 24)); - _ensure_glyph(fd, size, (int32_t)idx | (1 << 27) | (aa << 24)); - _ensure_glyph(fd, size, (int32_t)idx | (2 << 27) | (aa << 24)); - _ensure_glyph(fd, size, (int32_t)idx | (3 << 27) | (aa << 24)); + _ensure_glyph(fd, size, (int32_t)idx | (0 << 27) | (aa << 24), fgl); + _ensure_glyph(fd, size, (int32_t)idx | (1 << 27) | (aa << 24), fgl); + _ensure_glyph(fd, size, (int32_t)idx | (2 << 27) | (aa << 24), fgl); + _ensure_glyph(fd, size, (int32_t)idx | (3 << 27) | (aa << 24), fgl); } else if ((fd->subpixel_positioning == SUBPIXEL_POSITIONING_ONE_HALF) || (fd->subpixel_positioning == SUBPIXEL_POSITIONING_AUTO && size.x <= SUBPIXEL_POSITIONING_ONE_HALF_MAX_SIZE)) { - _ensure_glyph(fd, size, (int32_t)idx | (1 << 27) | (aa << 24)); - _ensure_glyph(fd, size, (int32_t)idx | (0 << 27) | (aa << 24)); + _ensure_glyph(fd, size, (int32_t)idx | (1 << 27) | (aa << 24), fgl); + _ensure_glyph(fd, size, (int32_t)idx | (0 << 27) | (aa << 24), fgl); } else { - _ensure_glyph(fd, size, (int32_t)idx | (aa << 24)); + _ensure_glyph(fd, size, (int32_t)idx | (aa << 24), fgl); } } } @@ -2583,16 +2648,17 @@ void TextServerFallback::_font_draw_glyph(const RID &p_font_rid, const RID &p_ca MutexLock lock(fd->mutex); Vector2i size = _get_size(fd, p_size); - ERR_FAIL_COND(!_ensure_cache_for_size(fd, size)); + FontForSizeFallback *ffsd = nullptr; + ERR_FAIL_COND(!_ensure_cache_for_size(fd, size, ffsd)); int32_t index = p_index & 0xffffff; // Remove subpixel shifts. bool lcd_aa = false; #ifdef MODULE_FREETYPE_ENABLED - if (!fd->msdf && fd->cache[size]->face) { + if (!fd->msdf && ffsd->face) { // LCD layout, bits 24, 25, 26 if (fd->antialiasing == FONT_ANTIALIASING_LCD) { - TextServer::FontLCDSubpixelLayout layout = (TextServer::FontLCDSubpixelLayout)(int)GLOBAL_GET("gui/theme/lcd_subpixel_layout"); + TextServer::FontLCDSubpixelLayout layout = lcd_subpixel_layout.get(); if (layout != FONT_LCD_SUBPIXEL_LAYOUT_NONE) { lcd_aa = true; index = index | (layout << 24); @@ -2609,24 +2675,24 @@ void TextServerFallback::_font_draw_glyph(const RID &p_font_rid, const RID &p_ca } #endif - if (!_ensure_glyph(fd, size, index)) { + FontGlyph fgl; + if (!_ensure_glyph(fd, size, index, fgl)) { return; // Invalid or non-graphical glyph, do not display errors, nothing to draw. } - const FontGlyph &gl = fd->cache[size]->glyph_map[index]; - if (gl.found) { - ERR_FAIL_COND(gl.texture_idx < -1 || gl.texture_idx >= fd->cache[size]->textures.size()); + if (fgl.found) { + ERR_FAIL_COND(fgl.texture_idx < -1 || fgl.texture_idx >= ffsd->textures.size()); - if (gl.texture_idx != -1) { + if (fgl.texture_idx != -1) { Color modulate = p_color; #ifdef MODULE_FREETYPE_ENABLED - if (fd->cache[size]->face && fd->cache[size]->textures[gl.texture_idx].image.is_valid() && (fd->cache[size]->textures[gl.texture_idx].image->get_format() == Image::FORMAT_RGBA8) && !lcd_aa && !fd->msdf) { + if (ffsd->face && ffsd->textures[fgl.texture_idx].image.is_valid() && (ffsd->textures[fgl.texture_idx].image->get_format() == Image::FORMAT_RGBA8) && !lcd_aa && !fd->msdf) { modulate.r = modulate.g = modulate.b = 1.0; } #endif if (RenderingServer::get_singleton() != nullptr) { - if (fd->cache[size]->textures[gl.texture_idx].dirty) { - ShelfPackTexture &tex = fd->cache[size]->textures.write[gl.texture_idx]; + if (ffsd->textures[fgl.texture_idx].dirty) { + ShelfPackTexture &tex = ffsd->textures.write[fgl.texture_idx]; Ref<Image> img = tex.image; if (fd->mipmaps && !img->has_mipmaps()) { img = tex.image->duplicate(); @@ -2639,12 +2705,12 @@ void TextServerFallback::_font_draw_glyph(const RID &p_font_rid, const RID &p_ca } tex.dirty = false; } - RID texture = fd->cache[size]->textures[gl.texture_idx].texture->get_rid(); + RID texture = ffsd->textures[fgl.texture_idx].texture->get_rid(); if (fd->msdf) { Point2 cpos = p_pos; - cpos += gl.rect.position * (double)p_size / (double)fd->msdf_source_size; - Size2 csize = gl.rect.size * (double)p_size / (double)fd->msdf_source_size; - RenderingServer::get_singleton()->canvas_item_add_msdf_texture_rect_region(p_canvas, Rect2(cpos, csize), texture, gl.uv_rect, modulate, 0, fd->msdf_range, (double)p_size / (double)fd->msdf_source_size); + cpos += fgl.rect.position * (double)p_size / (double)fd->msdf_source_size; + Size2 csize = fgl.rect.size * (double)p_size / (double)fd->msdf_source_size; + RenderingServer::get_singleton()->canvas_item_add_msdf_texture_rect_region(p_canvas, Rect2(cpos, csize), texture, fgl.uv_rect, modulate, 0, fd->msdf_range, (double)p_size / (double)fd->msdf_source_size); } else { Point2 cpos = p_pos; double scale = _font_get_scale(p_font_rid, p_size); @@ -2657,8 +2723,8 @@ void TextServerFallback::_font_draw_glyph(const RID &p_font_rid, const RID &p_ca cpos.y = Math::floor(cpos.y); cpos.x = Math::floor(cpos.x); } - Vector2 gpos = gl.rect.position; - Size2 csize = gl.rect.size; + Vector2 gpos = fgl.rect.position; + Size2 csize = fgl.rect.size; if (fd->fixed_size > 0 && fd->fixed_size_scale_mode != FIXED_SIZE_SCALE_DISABLE && size.x != p_size) { if (fd->fixed_size_scale_mode == FIXED_SIZE_SCALE_ENABLED) { double gl_scale = (double)p_size / (double)fd->fixed_size; @@ -2672,9 +2738,9 @@ void TextServerFallback::_font_draw_glyph(const RID &p_font_rid, const RID &p_ca } cpos += gpos; if (lcd_aa) { - RenderingServer::get_singleton()->canvas_item_add_lcd_texture_rect_region(p_canvas, Rect2(cpos, csize), texture, gl.uv_rect, modulate); + RenderingServer::get_singleton()->canvas_item_add_lcd_texture_rect_region(p_canvas, Rect2(cpos, csize), texture, fgl.uv_rect, modulate); } else { - RenderingServer::get_singleton()->canvas_item_add_texture_rect_region(p_canvas, Rect2(cpos, csize), texture, gl.uv_rect, modulate, false, false); + RenderingServer::get_singleton()->canvas_item_add_texture_rect_region(p_canvas, Rect2(cpos, csize), texture, fgl.uv_rect, modulate, false, false); } } } @@ -2691,16 +2757,17 @@ void TextServerFallback::_font_draw_glyph_outline(const RID &p_font_rid, const R MutexLock lock(fd->mutex); Vector2i size = _get_size_outline(fd, Vector2i(p_size, p_outline_size)); - ERR_FAIL_COND(!_ensure_cache_for_size(fd, size)); + FontForSizeFallback *ffsd = nullptr; + ERR_FAIL_COND(!_ensure_cache_for_size(fd, size, ffsd)); int32_t index = p_index & 0xffffff; // Remove subpixel shifts. bool lcd_aa = false; #ifdef MODULE_FREETYPE_ENABLED - if (!fd->msdf && fd->cache[size]->face) { + if (!fd->msdf && ffsd->face) { // LCD layout, bits 24, 25, 26 if (fd->antialiasing == FONT_ANTIALIASING_LCD) { - TextServer::FontLCDSubpixelLayout layout = (TextServer::FontLCDSubpixelLayout)(int)GLOBAL_GET("gui/theme/lcd_subpixel_layout"); + TextServer::FontLCDSubpixelLayout layout = lcd_subpixel_layout.get(); if (layout != FONT_LCD_SUBPIXEL_LAYOUT_NONE) { lcd_aa = true; index = index | (layout << 24); @@ -2717,24 +2784,24 @@ void TextServerFallback::_font_draw_glyph_outline(const RID &p_font_rid, const R } #endif - if (!_ensure_glyph(fd, size, index)) { + FontGlyph fgl; + if (!_ensure_glyph(fd, size, index, fgl)) { return; // Invalid or non-graphical glyph, do not display errors, nothing to draw. } - const FontGlyph &gl = fd->cache[size]->glyph_map[index]; - if (gl.found) { - ERR_FAIL_COND(gl.texture_idx < -1 || gl.texture_idx >= fd->cache[size]->textures.size()); + if (fgl.found) { + ERR_FAIL_COND(fgl.texture_idx < -1 || fgl.texture_idx >= ffsd->textures.size()); - if (gl.texture_idx != -1) { + if (fgl.texture_idx != -1) { Color modulate = p_color; #ifdef MODULE_FREETYPE_ENABLED - if (fd->cache[size]->face && fd->cache[size]->textures[gl.texture_idx].image.is_valid() && (fd->cache[size]->textures[gl.texture_idx].image->get_format() == Image::FORMAT_RGBA8) && !lcd_aa && !fd->msdf) { + if (ffsd->face && ffsd->textures[fgl.texture_idx].image.is_valid() && (ffsd->textures[fgl.texture_idx].image->get_format() == Image::FORMAT_RGBA8) && !lcd_aa && !fd->msdf) { modulate.r = modulate.g = modulate.b = 1.0; } #endif if (RenderingServer::get_singleton() != nullptr) { - if (fd->cache[size]->textures[gl.texture_idx].dirty) { - ShelfPackTexture &tex = fd->cache[size]->textures.write[gl.texture_idx]; + if (ffsd->textures[fgl.texture_idx].dirty) { + ShelfPackTexture &tex = ffsd->textures.write[fgl.texture_idx]; Ref<Image> img = tex.image; if (fd->mipmaps && !img->has_mipmaps()) { img = tex.image->duplicate(); @@ -2747,12 +2814,12 @@ void TextServerFallback::_font_draw_glyph_outline(const RID &p_font_rid, const R } tex.dirty = false; } - RID texture = fd->cache[size]->textures[gl.texture_idx].texture->get_rid(); + RID texture = ffsd->textures[fgl.texture_idx].texture->get_rid(); if (fd->msdf) { Point2 cpos = p_pos; - cpos += gl.rect.position * (double)p_size / (double)fd->msdf_source_size; - Size2 csize = gl.rect.size * (double)p_size / (double)fd->msdf_source_size; - RenderingServer::get_singleton()->canvas_item_add_msdf_texture_rect_region(p_canvas, Rect2(cpos, csize), texture, gl.uv_rect, modulate, p_outline_size, fd->msdf_range, (double)p_size / (double)fd->msdf_source_size); + cpos += fgl.rect.position * (double)p_size / (double)fd->msdf_source_size; + Size2 csize = fgl.rect.size * (double)p_size / (double)fd->msdf_source_size; + RenderingServer::get_singleton()->canvas_item_add_msdf_texture_rect_region(p_canvas, Rect2(cpos, csize), texture, fgl.uv_rect, modulate, p_outline_size, fd->msdf_range, (double)p_size / (double)fd->msdf_source_size); } else { Point2 cpos = p_pos; double scale = _font_get_scale(p_font_rid, p_size); @@ -2765,8 +2832,8 @@ void TextServerFallback::_font_draw_glyph_outline(const RID &p_font_rid, const R cpos.y = Math::floor(cpos.y); cpos.x = Math::floor(cpos.x); } - Vector2 gpos = gl.rect.position; - Size2 csize = gl.rect.size; + Vector2 gpos = fgl.rect.position; + Size2 csize = fgl.rect.size; if (fd->fixed_size > 0 && fd->fixed_size_scale_mode != FIXED_SIZE_SCALE_DISABLE && size.x != p_size) { if (fd->fixed_size_scale_mode == FIXED_SIZE_SCALE_ENABLED) { double gl_scale = (double)p_size / (double)fd->fixed_size; @@ -2780,9 +2847,9 @@ void TextServerFallback::_font_draw_glyph_outline(const RID &p_font_rid, const R } cpos += gpos; if (lcd_aa) { - RenderingServer::get_singleton()->canvas_item_add_lcd_texture_rect_region(p_canvas, Rect2(cpos, csize), texture, gl.uv_rect, modulate); + RenderingServer::get_singleton()->canvas_item_add_lcd_texture_rect_region(p_canvas, Rect2(cpos, csize), texture, fgl.uv_rect, modulate); } else { - RenderingServer::get_singleton()->canvas_item_add_texture_rect_region(p_canvas, Rect2(cpos, csize), texture, gl.uv_rect, modulate, false, false); + RenderingServer::get_singleton()->canvas_item_add_texture_rect_region(p_canvas, Rect2(cpos, csize), texture, fgl.uv_rect, modulate, false, false); } } } @@ -2872,7 +2939,8 @@ void TextServerFallback::_font_remove_script_support_override(const RID &p_font_ MutexLock lock(fd->mutex); Vector2i size = _get_size(fd, 16); - ERR_FAIL_COND(!_ensure_cache_for_size(fd, size)); + FontForSizeFallback *ffsd = nullptr; + ERR_FAIL_COND(!_ensure_cache_for_size(fd, size, ffsd)); fd->script_support_overrides.erase(p_script); } @@ -2894,7 +2962,8 @@ void TextServerFallback::_font_set_opentype_feature_overrides(const RID &p_font_ MutexLock lock(fd->mutex); Vector2i size = _get_size(fd, 16); - ERR_FAIL_COND(!_ensure_cache_for_size(fd, size)); + FontForSizeFallback *ffsd = nullptr; + ERR_FAIL_COND(!_ensure_cache_for_size(fd, size, ffsd)); fd->feature_overrides = p_overrides; } @@ -2916,7 +2985,8 @@ Dictionary TextServerFallback::_font_supported_variation_list(const RID &p_font_ MutexLock lock(fd->mutex); Vector2i size = _get_size(fd, 16); - ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size), Dictionary()); + FontForSizeFallback *ffsd = nullptr; + ERR_FAIL_COND_V(!_ensure_cache_for_size(fd, size, ffsd), Dictionary()); return fd->supported_varaitions; } @@ -2953,7 +3023,7 @@ void TextServerFallback::_font_set_global_oversampling(double p_oversampling) { /*************************************************************************/ void TextServerFallback::invalidate(ShapedTextDataFallback *p_shaped) { - p_shaped->valid = false; + p_shaped->valid.clear(); p_shaped->sort_valid = false; p_shaped->line_breaks_valid = false; p_shaped->justification_ops_valid = false; @@ -3192,7 +3262,7 @@ void TextServerFallback::_shaped_set_span_update_font(const RID &p_shaped, int64 span.font_size = p_size; span.features = p_opentype_features; - sd->valid = false; + sd->valid.clear(); } bool TextServerFallback::_shaped_text_add_string(const RID &p_shaped, const String &p_text, const TypedArray<RID> &p_fonts, int64_t p_size, const Dictionary &p_opentype_features, const String &p_language, const Variant &p_meta) { @@ -3288,7 +3358,7 @@ bool TextServerFallback::_shaped_text_resize_object(const RID &p_shaped, const V sd->objects[p_key].rect.size = p_size; sd->objects[p_key].inline_align = p_inline_align; sd->objects[p_key].baseline = p_baseline; - if (sd->valid) { + if (sd->valid.is_set()) { // Recalc string metrics. sd->ascent = 0; sd->descent = 0; @@ -3431,7 +3501,7 @@ RID TextServerFallback::_shaped_text_substr(const RID &p_shaped, int64_t p_start if (sd->parent != RID()) { return _shaped_text_substr(sd->parent, p_start, p_length); } - if (!sd->valid) { + if (!sd->valid.is_set()) { const_cast<TextServerFallback *>(this)->_shaped_text_shape(p_shaped); } ERR_FAIL_COND_V(p_start < 0 || p_length < 0, RID()); @@ -3514,7 +3584,7 @@ RID TextServerFallback::_shaped_text_substr(const RID &p_shaped, int64_t p_start _realign(new_sd); } - new_sd->valid = true; + new_sd->valid.set(); return shaped_owner.make_rid(new_sd); } @@ -3532,7 +3602,7 @@ double TextServerFallback::_shaped_text_fit_to_width(const RID &p_shaped, double ERR_FAIL_NULL_V(sd, 0.0); MutexLock lock(sd->mutex); - if (!sd->valid) { + if (!sd->valid.is_set()) { const_cast<TextServerFallback *>(this)->_shaped_text_shape(p_shaped); } if (!sd->justification_ops_valid) { @@ -3641,7 +3711,7 @@ double TextServerFallback::_shaped_text_tab_align(const RID &p_shaped, const Pac ERR_FAIL_NULL_V(sd, 0.0); MutexLock lock(sd->mutex); - if (!sd->valid) { + if (!sd->valid.is_set()) { const_cast<TextServerFallback *>(this)->_shaped_text_shape(p_shaped); } if (!sd->line_breaks_valid) { @@ -3697,7 +3767,7 @@ bool TextServerFallback::_shaped_text_update_breaks(const RID &p_shaped) { ERR_FAIL_NULL_V(sd, false); MutexLock lock(sd->mutex); - if (!sd->valid) { + if (!sd->valid.is_set()) { _shaped_text_shape(p_shaped); } @@ -3761,7 +3831,7 @@ bool TextServerFallback::_shaped_text_update_justification_ops(const RID &p_shap ERR_FAIL_NULL_V(sd, false); MutexLock lock(sd->mutex); - if (!sd->valid) { + if (!sd->valid.is_set()) { _shaped_text_shape(p_shaped); } if (!sd->line_breaks_valid) { @@ -3940,7 +4010,7 @@ void TextServerFallback::_shaped_text_overrun_trim_to_width(const RID &p_shaped_ ERR_FAIL_NULL_MSG(sd, "ShapedTextDataFallback invalid."); MutexLock lock(sd->mutex); - if (!sd->valid) { + if (!sd->valid.is_set()) { _shaped_text_shape(p_shaped_line); } @@ -3967,7 +4037,7 @@ void TextServerFallback::_shaped_text_overrun_trim_to_width(const RID &p_shaped_ Vector<ShapedTextDataFallback::Span> &spans = sd->spans; if (sd->parent != RID()) { ShapedTextDataFallback *parent_sd = shaped_owner.get_or_null(sd->parent); - ERR_FAIL_COND(!parent_sd->valid); + ERR_FAIL_COND(!parent_sd->valid.is_set()); spans = parent_sd->spans; } @@ -4161,7 +4231,7 @@ bool TextServerFallback::_shaped_text_shape(const RID &p_shaped) { ERR_FAIL_NULL_V(sd, false); MutexLock lock(sd->mutex); - if (sd->valid) { + if (sd->valid.is_set()) { return true; } @@ -4178,7 +4248,7 @@ bool TextServerFallback::_shaped_text_shape(const RID &p_shaped) { sd->glyphs.clear(); if (sd->text.length() == 0) { - sd->valid = true; + sd->valid.set(); return true; } @@ -4307,16 +4377,15 @@ bool TextServerFallback::_shaped_text_shape(const RID &p_shaped) { // Align embedded objects to baseline. _realign(sd); - sd->valid = true; - return sd->valid; + sd->valid.set(); + return sd->valid.is_set(); } bool TextServerFallback::_shaped_text_is_ready(const RID &p_shaped) const { const ShapedTextDataFallback *sd = shaped_owner.get_or_null(p_shaped); ERR_FAIL_NULL_V(sd, false); - MutexLock lock(sd->mutex); - return sd->valid; + return sd->valid.is_set(); } const Glyph *TextServerFallback::_shaped_text_get_glyphs(const RID &p_shaped) const { @@ -4324,7 +4393,7 @@ const Glyph *TextServerFallback::_shaped_text_get_glyphs(const RID &p_shaped) co ERR_FAIL_NULL_V(sd, nullptr); MutexLock lock(sd->mutex); - if (!sd->valid) { + if (!sd->valid.is_set()) { const_cast<TextServerFallback *>(this)->_shaped_text_shape(p_shaped); } return sd->glyphs.ptr(); @@ -4335,7 +4404,7 @@ int64_t TextServerFallback::_shaped_text_get_glyph_count(const RID &p_shaped) co ERR_FAIL_NULL_V(sd, 0); MutexLock lock(sd->mutex); - if (!sd->valid) { + if (!sd->valid.is_set()) { const_cast<TextServerFallback *>(this)->_shaped_text_shape(p_shaped); } return sd->glyphs.size(); @@ -4346,7 +4415,7 @@ const Glyph *TextServerFallback::_shaped_text_sort_logical(const RID &p_shaped) ERR_FAIL_NULL_V(sd, nullptr); MutexLock lock(sd->mutex); - if (!sd->valid) { + if (!sd->valid.is_set()) { const_cast<TextServerFallback *>(this)->_shaped_text_shape(p_shaped); } @@ -4380,7 +4449,7 @@ Rect2 TextServerFallback::_shaped_text_get_object_rect(const RID &p_shaped, cons MutexLock lock(sd->mutex); ERR_FAIL_COND_V(!sd->objects.has(p_key), Rect2()); - if (!sd->valid) { + if (!sd->valid.is_set()) { const_cast<TextServerFallback *>(this)->_shaped_text_shape(p_shaped); } return sd->objects[p_key].rect; @@ -4417,7 +4486,7 @@ Size2 TextServerFallback::_shaped_text_get_size(const RID &p_shaped) const { ERR_FAIL_NULL_V(sd, Size2()); MutexLock lock(sd->mutex); - if (!sd->valid) { + if (!sd->valid.is_set()) { const_cast<TextServerFallback *>(this)->_shaped_text_shape(p_shaped); } if (sd->orientation == TextServer::ORIENTATION_HORIZONTAL) { @@ -4432,7 +4501,7 @@ double TextServerFallback::_shaped_text_get_ascent(const RID &p_shaped) const { ERR_FAIL_NULL_V(sd, 0.0); MutexLock lock(sd->mutex); - if (!sd->valid) { + if (!sd->valid.is_set()) { const_cast<TextServerFallback *>(this)->_shaped_text_shape(p_shaped); } return sd->ascent + sd->extra_spacing[SPACING_TOP]; @@ -4443,7 +4512,7 @@ double TextServerFallback::_shaped_text_get_descent(const RID &p_shaped) const { ERR_FAIL_NULL_V(sd, 0.0); MutexLock lock(sd->mutex); - if (!sd->valid) { + if (!sd->valid.is_set()) { const_cast<TextServerFallback *>(this)->_shaped_text_shape(p_shaped); } return sd->descent + sd->extra_spacing[SPACING_BOTTOM]; @@ -4454,7 +4523,7 @@ double TextServerFallback::_shaped_text_get_width(const RID &p_shaped) const { ERR_FAIL_NULL_V(sd, 0.0); MutexLock lock(sd->mutex); - if (!sd->valid) { + if (!sd->valid.is_set()) { const_cast<TextServerFallback *>(this)->_shaped_text_shape(p_shaped); } return Math::ceil(sd->width); @@ -4465,7 +4534,7 @@ double TextServerFallback::_shaped_text_get_underline_position(const RID &p_shap ERR_FAIL_NULL_V(sd, 0.0); MutexLock lock(sd->mutex); - if (!sd->valid) { + if (!sd->valid.is_set()) { const_cast<TextServerFallback *>(this)->_shaped_text_shape(p_shaped); } @@ -4477,7 +4546,7 @@ double TextServerFallback::_shaped_text_get_underline_thickness(const RID &p_sha ERR_FAIL_NULL_V(sd, 0.0); MutexLock lock(sd->mutex); - if (!sd->valid) { + if (!sd->valid.is_set()) { const_cast<TextServerFallback *>(this)->_shaped_text_shape(p_shaped); } @@ -4489,7 +4558,7 @@ PackedInt32Array TextServerFallback::_shaped_text_get_character_breaks(const RID ERR_FAIL_NULL_V(sd, PackedInt32Array()); MutexLock lock(sd->mutex); - if (!sd->valid) { + if (!sd->valid.is_set()) { const_cast<TextServerFallback *>(this)->_shaped_text_shape(p_shaped); } @@ -4626,8 +4695,13 @@ PackedInt32Array TextServerFallback::_string_get_word_breaks(const String &p_str return ret; } +void TextServerFallback::_update_settings() { + lcd_subpixel_layout.set((TextServer::FontLCDSubpixelLayout)(int)GLOBAL_GET("gui/theme/lcd_subpixel_layout")); +} + TextServerFallback::TextServerFallback() { _insert_feature_sets(); + ProjectSettings::get_singleton()->connect("settings_changed", callable_mp(this, &TextServerFallback::_update_settings)); }; void TextServerFallback::_cleanup() { diff --git a/modules/text_server_fb/text_server_fb.h b/modules/text_server_fb/text_server_fb.h index 1b76c6fa0f..ee1f72401f 100644 --- a/modules/text_server_fb/text_server_fb.h +++ b/modules/text_server_fb/text_server_fb.h @@ -72,6 +72,7 @@ #include <godot_cpp/templates/hash_map.hpp> #include <godot_cpp/templates/hash_set.hpp> #include <godot_cpp/templates/rid_owner.hpp> +#include <godot_cpp/templates/safe_refcount.hpp> #include <godot_cpp/templates/vector.hpp> using namespace godot; @@ -83,6 +84,7 @@ using namespace godot; #include "core/object/worker_thread_pool.h" #include "core/templates/hash_map.h" #include "core/templates/rid_owner.h" +#include "core/templates/safe_refcount.h" #include "scene/resources/image_texture.h" #include "servers/text/text_server_extension.h" @@ -116,6 +118,9 @@ class TextServerFallback : public TextServerExtension { HashMap<StringName, int32_t> feature_sets; HashMap<int32_t, StringName> feature_sets_inv; + SafeNumeric<TextServer::FontLCDSubpixelLayout> lcd_subpixel_layout{ TextServer::FontLCDSubpixelLayout::FONT_LCD_SUBPIXEL_LAYOUT_NONE }; + void _update_settings(); + void _insert_feature_sets(); _FORCE_INLINE_ void _insert_feature(const StringName &p_name, int32_t p_tag); @@ -278,7 +283,7 @@ class TextServerFallback : public TextServerExtension { int extra_spacing[4] = { 0, 0, 0, 0 }; double baseline_offset = 0.0; - HashMap<Vector2i, FontForSizeFallback *, VariantHasher, VariantComparator> cache; + HashMap<Vector2i, FontForSizeFallback *> cache; bool face_init = false; Dictionary supported_varaitions; @@ -308,8 +313,8 @@ class TextServerFallback : public TextServerExtension { #ifdef MODULE_FREETYPE_ENABLED _FORCE_INLINE_ FontGlyph rasterize_bitmap(FontForSizeFallback *p_data, int p_rect_margin, FT_Bitmap p_bitmap, int p_yofs, int p_xofs, const Vector2 &p_advance, bool p_bgra) const; #endif - _FORCE_INLINE_ bool _ensure_glyph(FontFallback *p_font_data, const Vector2i &p_size, int32_t p_glyph) const; - _FORCE_INLINE_ bool _ensure_cache_for_size(FontFallback *p_font_data, const Vector2i &p_size) const; + _FORCE_INLINE_ bool _ensure_glyph(FontFallback *p_font_data, const Vector2i &p_size, int32_t p_glyph, FontGlyph &r_glyph) const; + _FORCE_INLINE_ bool _ensure_cache_for_size(FontFallback *p_font_data, const Vector2i &p_size, FontForSizeFallback *&r_cache_for_size) const; _FORCE_INLINE_ void _font_clear_cache(FontFallback *p_font_data); static void _generateMTSDF_threaded(void *p_td, uint32_t p_y); @@ -432,7 +437,7 @@ class TextServerFallback : public TextServerExtension { /* Shaped data */ TextServer::Direction para_direction = DIRECTION_LTR; // Detected text direction. - bool valid = false; // String is shaped. + SafeFlag valid{ false }; // String is shaped. bool line_breaks_valid = false; // Line and word break flags are populated (and virtual zero width spaces inserted). bool justification_ops_valid = false; // Virtual elongation glyphs are added to the string. bool sort_valid = false; diff --git a/modules/upnp/doc_classes/UPNP.xml b/modules/upnp/doc_classes/UPNP.xml index 7eba3ad8ec..4b5ad07688 100644 --- a/modules/upnp/doc_classes/UPNP.xml +++ b/modules/upnp/doc_classes/UPNP.xml @@ -31,13 +31,13 @@ if err != OK: push_error(str(err)) - emit_signal("upnp_completed", err) + upnp_completed.emit(err) return if upnp.get_gateway() and upnp.get_gateway().is_valid_gateway(): upnp.add_port_mapping(server_port, server_port, ProjectSettings.get_setting("application/config/name"), "UDP") upnp.add_port_mapping(server_port, server_port, ProjectSettings.get_setting("application/config/name"), "TCP") - emit_signal("upnp_completed", OK) + upnp_completed.emit(OK) func _ready(): thread = Thread.new() diff --git a/modules/webrtc/webrtc_peer_connection.cpp b/modules/webrtc/webrtc_peer_connection.cpp index 0a50b677c4..69be873fcf 100644 --- a/modules/webrtc/webrtc_peer_connection.cpp +++ b/modules/webrtc/webrtc_peer_connection.cpp @@ -43,15 +43,20 @@ void WebRTCPeerConnection::set_default_extension(const StringName &p_extension) default_extension = StringName(p_extension, true); } -WebRTCPeerConnection *WebRTCPeerConnection::create() { +WebRTCPeerConnection *WebRTCPeerConnection::create(bool p_notify_postinitialize) { #ifdef WEB_ENABLED - return memnew(WebRTCPeerConnectionJS); + return static_cast<WebRTCPeerConnection *>(ClassDB::creator<WebRTCPeerConnectionJS>(p_notify_postinitialize)); #else if (default_extension == StringName()) { WARN_PRINT_ONCE("No default WebRTC extension configured."); - return memnew(WebRTCPeerConnectionExtension); + return static_cast<WebRTCPeerConnection *>(ClassDB::creator<WebRTCPeerConnectionExtension>(p_notify_postinitialize)); + } + Object *obj = nullptr; + if (p_notify_postinitialize) { + obj = ClassDB::instantiate(default_extension); + } else { + obj = ClassDB::instantiate_without_postinitialization(default_extension); } - Object *obj = ClassDB::instantiate(default_extension); return Object::cast_to<WebRTCPeerConnectionExtension>(obj); #endif } diff --git a/modules/webrtc/webrtc_peer_connection.h b/modules/webrtc/webrtc_peer_connection.h index 0f79c17519..33c95ccd0f 100644 --- a/modules/webrtc/webrtc_peer_connection.h +++ b/modules/webrtc/webrtc_peer_connection.h @@ -85,7 +85,7 @@ public: virtual Error poll() = 0; virtual void close() = 0; - static WebRTCPeerConnection *create(); + static WebRTCPeerConnection *create(bool p_notify_postinitialize = true); WebRTCPeerConnection(); ~WebRTCPeerConnection(); diff --git a/modules/websocket/emws_peer.h b/modules/websocket/emws_peer.h index 38f15c82e5..fe0bc594e6 100644 --- a/modules/websocket/emws_peer.h +++ b/modules/websocket/emws_peer.h @@ -68,7 +68,7 @@ private: String selected_protocol; String requested_url; - static WebSocketPeer *_create() { return memnew(EMWSPeer); } + static WebSocketPeer *_create(bool p_notify_postinitialize) { return static_cast<WebSocketPeer *>(ClassDB::creator<EMWSPeer>(p_notify_postinitialize)); } static void _esws_on_connect(void *obj, char *proto); static void _esws_on_message(void *obj, const uint8_t *p_data, int p_data_size, int p_is_string); static void _esws_on_error(void *obj); diff --git a/modules/websocket/websocket_peer.cpp b/modules/websocket/websocket_peer.cpp index 3c0d316bc9..95a1a238e9 100644 --- a/modules/websocket/websocket_peer.cpp +++ b/modules/websocket/websocket_peer.cpp @@ -30,7 +30,7 @@ #include "websocket_peer.h" -WebSocketPeer *(*WebSocketPeer::_create)() = nullptr; +WebSocketPeer *(*WebSocketPeer::_create)(bool p_notify_postinitialize) = nullptr; WebSocketPeer::WebSocketPeer() { } diff --git a/modules/websocket/websocket_peer.h b/modules/websocket/websocket_peer.h index 3110e87071..ef0197cf6c 100644 --- a/modules/websocket/websocket_peer.h +++ b/modules/websocket/websocket_peer.h @@ -59,7 +59,7 @@ private: virtual Error _send_bind(const PackedByteArray &p_data, WriteMode p_mode = WRITE_MODE_BINARY); protected: - static WebSocketPeer *(*_create)(); + static WebSocketPeer *(*_create)(bool p_notify_postinitialize); static void _bind_methods(); @@ -74,11 +74,11 @@ protected: int max_queued_packets = 2048; public: - static WebSocketPeer *create() { + static WebSocketPeer *create(bool p_notify_postinitialize = true) { if (!_create) { return nullptr; } - return _create(); + return _create(p_notify_postinitialize); } virtual Error connect_to_url(const String &p_url, Ref<TLSOptions> p_options = Ref<TLSOptions>()) = 0; diff --git a/modules/websocket/wsl_peer.h b/modules/websocket/wsl_peer.h index bf9f5c8527..fb01da7ce2 100644 --- a/modules/websocket/wsl_peer.h +++ b/modules/websocket/wsl_peer.h @@ -49,7 +49,7 @@ class WSLPeer : public WebSocketPeer { private: static CryptoCore::RandomGenerator *_static_rng; - static WebSocketPeer *_create() { return memnew(WSLPeer); } + static WebSocketPeer *_create(bool p_notify_postinitialize) { return static_cast<WebSocketPeer *>(ClassDB::creator<WSLPeer>(p_notify_postinitialize)); } // Callbacks. static ssize_t _wsl_recv_callback(wslay_event_context_ptr ctx, uint8_t *data, size_t len, int flags, void *user_data); diff --git a/platform/SCsub b/platform/SCsub index b07023efed..cdaa6074ba 100644 --- a/platform/SCsub +++ b/platform/SCsub @@ -15,12 +15,12 @@ def export_icon_builder(target, source, env): src_path = Path(str(source[0])) src_name = src_path.stem platform = src_path.parent.parent.stem - with open(str(source[0]), "rb") as file: - svg = "".join([f"\\{hex(x)[1:]}" for x in file.read()]) + with open(str(source[0]), "r") as file: + svg = file.read() with methods.generated_wrapper(target, prefix=platform) as file: file.write( f"""\ -static const char *_{platform}_{src_name}_svg = "{svg}"; +static const char *_{platform}_{src_name}_svg = {methods.to_raw_cstring(svg)}; """ ) diff --git a/platform/android/dir_access_jandroid.cpp b/platform/android/dir_access_jandroid.cpp index ab90527bfa..19c18eb96e 100644 --- a/platform/android/dir_access_jandroid.cpp +++ b/platform/android/dir_access_jandroid.cpp @@ -68,7 +68,7 @@ String DirAccessJAndroid::get_next() { if (_dir_next) { JNIEnv *env = get_jni_env(); ERR_FAIL_NULL_V(env, ""); - jstring str = (jstring)env->CallObjectMethod(dir_access_handler, _dir_next, get_access_type(), id); + jstring str = (jstring)env->CallObjectMethod(dir_access_handler, _dir_next, id); if (!str) { return ""; } @@ -85,7 +85,7 @@ bool DirAccessJAndroid::current_is_dir() const { if (_dir_is_dir) { JNIEnv *env = get_jni_env(); ERR_FAIL_NULL_V(env, false); - return env->CallBooleanMethod(dir_access_handler, _dir_is_dir, get_access_type(), id); + return env->CallBooleanMethod(dir_access_handler, _dir_is_dir, id); } else { return false; } @@ -95,7 +95,7 @@ bool DirAccessJAndroid::current_is_hidden() const { if (_current_is_hidden) { JNIEnv *env = get_jni_env(); ERR_FAIL_NULL_V(env, false); - return env->CallBooleanMethod(dir_access_handler, _current_is_hidden, get_access_type(), id); + return env->CallBooleanMethod(dir_access_handler, _current_is_hidden, id); } return false; } @@ -218,7 +218,7 @@ bool DirAccessJAndroid::dir_exists(String p_dir) { } } -Error DirAccessJAndroid::make_dir_recursive(const String &p_dir) { +Error DirAccessJAndroid::make_dir(String p_dir) { // Check if the directory exists already if (dir_exists(p_dir)) { return ERR_ALREADY_EXISTS; @@ -242,8 +242,12 @@ Error DirAccessJAndroid::make_dir_recursive(const String &p_dir) { } } -Error DirAccessJAndroid::make_dir(String p_dir) { - return make_dir_recursive(p_dir); +Error DirAccessJAndroid::make_dir_recursive(const String &p_dir) { + Error err = make_dir(p_dir); + if (err != OK && err != ERR_ALREADY_EXISTS) { + ERR_FAIL_V_MSG(err, "Could not create directory: " + p_dir); + } + return OK; } Error DirAccessJAndroid::rename(String p_from, String p_to) { @@ -307,9 +311,9 @@ void DirAccessJAndroid::setup(jobject p_dir_access_handler) { cls = (jclass)env->NewGlobalRef(c); _dir_open = env->GetMethodID(cls, "dirOpen", "(ILjava/lang/String;)I"); - _dir_next = env->GetMethodID(cls, "dirNext", "(II)Ljava/lang/String;"); - _dir_close = env->GetMethodID(cls, "dirClose", "(II)V"); - _dir_is_dir = env->GetMethodID(cls, "dirIsDir", "(II)Z"); + _dir_next = env->GetMethodID(cls, "dirNext", "(I)Ljava/lang/String;"); + _dir_close = env->GetMethodID(cls, "dirClose", "(I)V"); + _dir_is_dir = env->GetMethodID(cls, "dirIsDir", "(I)Z"); _dir_exists = env->GetMethodID(cls, "dirExists", "(ILjava/lang/String;)Z"); _file_exists = env->GetMethodID(cls, "fileExists", "(ILjava/lang/String;)Z"); _get_drive_count = env->GetMethodID(cls, "getDriveCount", "(I)I"); @@ -318,7 +322,7 @@ void DirAccessJAndroid::setup(jobject p_dir_access_handler) { _get_space_left = env->GetMethodID(cls, "getSpaceLeft", "(I)J"); _rename = env->GetMethodID(cls, "rename", "(ILjava/lang/String;Ljava/lang/String;)Z"); _remove = env->GetMethodID(cls, "remove", "(ILjava/lang/String;)Z"); - _current_is_hidden = env->GetMethodID(cls, "isCurrentHidden", "(II)Z"); + _current_is_hidden = env->GetMethodID(cls, "isCurrentHidden", "(I)Z"); } void DirAccessJAndroid::terminate() { @@ -355,6 +359,6 @@ void DirAccessJAndroid::dir_close(int p_id) { if (_dir_close) { JNIEnv *env = get_jni_env(); ERR_FAIL_NULL(env); - env->CallVoidMethod(dir_access_handler, _dir_close, get_access_type(), p_id); + env->CallVoidMethod(dir_access_handler, _dir_close, p_id); } } diff --git a/platform/android/dir_access_jandroid.h b/platform/android/dir_access_jandroid.h index 68578b0fa9..1d8fe906f3 100644 --- a/platform/android/dir_access_jandroid.h +++ b/platform/android/dir_access_jandroid.h @@ -84,7 +84,7 @@ public: virtual bool is_link(String p_file) override { return false; } virtual String read_link(String p_file) override { return p_file; } - virtual Error create_link(String p_source, String p_target) override { return FAILED; } + virtual Error create_link(String p_source, String p_target) override { return ERR_UNAVAILABLE; } virtual uint64_t get_space_left() override; diff --git a/platform/android/display_server_android.cpp b/platform/android/display_server_android.cpp index 8dc0e869d0..5bb520bd73 100644 --- a/platform/android/display_server_android.cpp +++ b/platform/android/display_server_android.cpp @@ -455,11 +455,15 @@ Size2i DisplayServerAndroid::window_get_size_with_decorations(DisplayServer::Win } void DisplayServerAndroid::window_set_mode(DisplayServer::WindowMode p_mode, DisplayServer::WindowID p_window) { - // Not supported on Android. + OS_Android::get_singleton()->get_godot_java()->enable_immersive_mode(p_mode == WINDOW_MODE_FULLSCREEN || p_mode == WINDOW_MODE_EXCLUSIVE_FULLSCREEN); } DisplayServer::WindowMode DisplayServerAndroid::window_get_mode(DisplayServer::WindowID p_window) const { - return WINDOW_MODE_FULLSCREEN; + if (OS_Android::get_singleton()->get_godot_java()->is_in_immersive_mode()) { + return WINDOW_MODE_FULLSCREEN; + } else { + return WINDOW_MODE_MAXIMIZED; + } } bool DisplayServerAndroid::window_is_maximize_allowed(DisplayServer::WindowID p_window) const { diff --git a/platform/android/export/export.cpp b/platform/android/export/export.cpp index 6a6d7149ff..3f4624d09c 100644 --- a/platform/android/export/export.cpp +++ b/platform/android/export/export.cpp @@ -42,16 +42,17 @@ void register_android_exporter_types() { } void register_android_exporter() { -#ifndef ANDROID_ENABLED - EDITOR_DEF("export/android/java_sdk_path", OS::get_singleton()->get_environment("JAVA_HOME")); - EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::STRING, "export/android/java_sdk_path", PROPERTY_HINT_GLOBAL_DIR)); - EDITOR_DEF("export/android/android_sdk_path", OS::get_singleton()->get_environment("ANDROID_HOME")); - EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::STRING, "export/android/android_sdk_path", PROPERTY_HINT_GLOBAL_DIR)); EDITOR_DEF("export/android/debug_keystore", EditorPaths::get_singleton()->get_debug_keystore_path()); EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::STRING, "export/android/debug_keystore", PROPERTY_HINT_GLOBAL_FILE, "*.keystore,*.jks")); EDITOR_DEF("export/android/debug_keystore_user", DEFAULT_ANDROID_KEYSTORE_DEBUG_USER); EDITOR_DEF("export/android/debug_keystore_pass", DEFAULT_ANDROID_KEYSTORE_DEBUG_PASSWORD); EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::STRING, "export/android/debug_keystore_pass", PROPERTY_HINT_PASSWORD)); + +#ifndef ANDROID_ENABLED + EDITOR_DEF("export/android/java_sdk_path", OS::get_singleton()->get_environment("JAVA_HOME")); + EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::STRING, "export/android/java_sdk_path", PROPERTY_HINT_GLOBAL_DIR)); + EDITOR_DEF("export/android/android_sdk_path", OS::get_singleton()->get_environment("ANDROID_HOME")); + EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::STRING, "export/android/android_sdk_path", PROPERTY_HINT_GLOBAL_DIR)); EDITOR_DEF("export/android/force_system_user", false); EDITOR_DEF("export/android/shutdown_adb_on_exit", true); diff --git a/platform/android/export/export_plugin.cpp b/platform/android/export/export_plugin.cpp index 689360aef6..f8ac591a78 100644 --- a/platform/android/export/export_plugin.cpp +++ b/platform/android/export/export_plugin.cpp @@ -57,6 +57,10 @@ #include "modules/svg/image_loader_svg.h" #endif +#ifdef ANDROID_ENABLED +#include "../os_android.h" +#endif + #include <string.h> static const char *android_perms[] = { @@ -1998,7 +2002,7 @@ String EditorExportPlatformAndroid::get_device_architecture(int p_index) const { return devices[p_index].architecture; } -Error EditorExportPlatformAndroid::run(const Ref<EditorExportPreset> &p_preset, int p_device, int p_debug_flags) { +Error EditorExportPlatformAndroid::run(const Ref<EditorExportPreset> &p_preset, int p_device, BitField<EditorExportPlatform::DebugFlags> p_debug_flags) { ERR_FAIL_INDEX_V(p_device, devices.size(), ERR_INVALID_PARAMETER); String can_export_error; @@ -2020,11 +2024,11 @@ Error EditorExportPlatformAndroid::run(const Ref<EditorExportPreset> &p_preset, } const bool use_wifi_for_remote_debug = EDITOR_GET("export/android/use_wifi_for_remote_debug"); - const bool use_remote = (p_debug_flags & DEBUG_FLAG_REMOTE_DEBUG) || (p_debug_flags & DEBUG_FLAG_DUMB_CLIENT); + const bool use_remote = p_debug_flags.has_flag(DEBUG_FLAG_REMOTE_DEBUG) || p_debug_flags.has_flag(DEBUG_FLAG_DUMB_CLIENT); const bool use_reverse = devices[p_device].api_level >= 21 && !use_wifi_for_remote_debug; if (use_reverse) { - p_debug_flags |= DEBUG_FLAG_REMOTE_DEBUG_LOCALHOST; + p_debug_flags.set_flag(DEBUG_FLAG_REMOTE_DEBUG_LOCALHOST); } String tmp_export_path = EditorPaths::get_singleton()->get_cache_dir().path_join("tmpexport." + uitos(OS::get_singleton()->get_unix_time()) + ".apk"); @@ -2103,7 +2107,7 @@ Error EditorExportPlatformAndroid::run(const Ref<EditorExportPreset> &p_preset, OS::get_singleton()->execute(adb, args, &output, &rv, true); print_verbose(output); - if (p_debug_flags & DEBUG_FLAG_REMOTE_DEBUG) { + if (p_debug_flags.has_flag(DEBUG_FLAG_REMOTE_DEBUG)) { int dbg_port = EDITOR_GET("network/debug/remote_port"); args.clear(); args.push_back("-s"); @@ -2118,7 +2122,7 @@ Error EditorExportPlatformAndroid::run(const Ref<EditorExportPreset> &p_preset, print_line("Reverse result: " + itos(rv)); } - if (p_debug_flags & DEBUG_FLAG_DUMB_CLIENT) { + if (p_debug_flags.has_flag(DEBUG_FLAG_DUMB_CLIENT)) { int fs_port = EDITOR_GET("filesystem/file_server/port"); args.clear(); @@ -2417,6 +2421,10 @@ bool EditorExportPlatformAndroid::has_valid_export_configuration(const Ref<Edito err += template_err; } } else { +#ifdef ANDROID_ENABLED + err += TTR("Gradle build is not supported for the Android editor.") + "\n"; + valid = false; +#else // Validate the custom gradle android source template. bool android_source_template_valid = false; const String android_source_template = p_preset->get("gradle_build/android_source_template"); @@ -2439,6 +2447,7 @@ bool EditorExportPlatformAndroid::has_valid_export_configuration(const Ref<Edito } valid = installed_android_build_template && !r_missing_templates; +#endif } // Validate the rest of the export configuration. @@ -2475,6 +2484,7 @@ bool EditorExportPlatformAndroid::has_valid_export_configuration(const Ref<Edito err += TTR("Release keystore incorrectly configured in the export preset.") + "\n"; } +#ifndef ANDROID_ENABLED String java_sdk_path = EDITOR_GET("export/android/java_sdk_path"); if (java_sdk_path.is_empty()) { err += TTR("A valid Java SDK path is required in Editor Settings.") + "\n"; @@ -2547,6 +2557,7 @@ bool EditorExportPlatformAndroid::has_valid_export_configuration(const Ref<Edito valid = false; } } +#endif if (!err.is_empty()) { r_error = err; @@ -2656,7 +2667,7 @@ Error EditorExportPlatformAndroid::save_apk_expansion_file(const Ref<EditorExpor return err; } -void EditorExportPlatformAndroid::get_command_line_flags(const Ref<EditorExportPreset> &p_preset, const String &p_path, int p_flags, Vector<uint8_t> &r_command_line_flags) { +void EditorExportPlatformAndroid::get_command_line_flags(const Ref<EditorExportPreset> &p_preset, const String &p_path, BitField<EditorExportPlatform::DebugFlags> p_flags, Vector<uint8_t> &r_command_line_flags) { String cmdline = p_preset->get("command_line/extra_args"); Vector<String> command_line_strings = cmdline.strip_edges().split(" "); for (int i = 0; i < command_line_strings.size(); i++) { @@ -2666,7 +2677,7 @@ void EditorExportPlatformAndroid::get_command_line_flags(const Ref<EditorExportP } } - gen_export_flags(command_line_strings, p_flags); + command_line_strings.append_array(gen_export_flags(p_flags)); bool apk_expansion = p_preset->get("apk_expansion/enable"); if (apk_expansion) { @@ -2689,7 +2700,7 @@ void EditorExportPlatformAndroid::get_command_line_flags(const Ref<EditorExportP bool immersive = p_preset->get("screen/immersive_mode"); if (immersive) { - command_line_strings.push_back("--use_immersive"); + command_line_strings.push_back("--fullscreen"); } bool debug_opengl = p_preset->get("graphics/opengl_debug"); @@ -2717,23 +2728,9 @@ void EditorExportPlatformAndroid::get_command_line_flags(const Ref<EditorExportP Error EditorExportPlatformAndroid::sign_apk(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &export_path, EditorProgress &ep) { int export_format = int(p_preset->get("gradle_build/export_format")); - String export_label = export_format == EXPORT_FORMAT_AAB ? "AAB" : "APK"; - String release_keystore = _get_keystore_path(p_preset, false); - String release_username = p_preset->get_or_env("keystore/release_user", ENV_ANDROID_KEYSTORE_RELEASE_USER); - String release_password = p_preset->get_or_env("keystore/release_password", ENV_ANDROID_KEYSTORE_RELEASE_PASS); - String target_sdk_version = p_preset->get("gradle_build/target_sdk"); - if (!target_sdk_version.is_valid_int()) { - target_sdk_version = itos(DEFAULT_TARGET_SDK_VERSION); - } - String apksigner = get_apksigner_path(target_sdk_version.to_int(), true); - print_verbose("Starting signing of the " + export_label + " binary using " + apksigner); - if (apksigner == "<FAILED>") { - add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("All 'apksigner' tools located in Android SDK 'build-tools' directory failed to execute. Please check that you have the correct version installed for your target sdk version. The resulting %s is unsigned."), export_label)); - return OK; - } - if (!FileAccess::exists(apksigner)) { - add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("'apksigner' could not be found. Please check that the command is available in the Android SDK build-tools directory. The resulting %s is unsigned."), export_label)); - return OK; + if (export_format == EXPORT_FORMAT_AAB) { + add_message(EXPORT_MESSAGE_ERROR, TTR("Code Signing"), TTR("AAB signing is not supported")); + return FAILED; } String keystore; @@ -2750,15 +2747,15 @@ Error EditorExportPlatformAndroid::sign_apk(const Ref<EditorExportPreset> &p_pre user = EDITOR_GET("export/android/debug_keystore_user"); } - if (ep.step(vformat(TTR("Signing debug %s..."), export_label), 104)) { + if (ep.step(TTR("Signing debug APK..."), 104)) { return ERR_SKIP; } } else { - keystore = release_keystore; - password = release_password; - user = release_username; + keystore = _get_keystore_path(p_preset, false); + password = p_preset->get_or_env("keystore/release_password", ENV_ANDROID_KEYSTORE_RELEASE_PASS); + user = p_preset->get_or_env("keystore/release_user", ENV_ANDROID_KEYSTORE_RELEASE_USER); - if (ep.step(vformat(TTR("Signing release %s..."), export_label), 104)) { + if (ep.step(TTR("Signing release APK..."), 104)) { return ERR_SKIP; } } @@ -2768,6 +2765,36 @@ Error EditorExportPlatformAndroid::sign_apk(const Ref<EditorExportPreset> &p_pre return ERR_FILE_CANT_OPEN; } + String apk_path = export_path; + if (apk_path.is_relative_path()) { + apk_path = OS::get_singleton()->get_resource_dir().path_join(apk_path); + } + apk_path = ProjectSettings::get_singleton()->globalize_path(apk_path).simplify_path(); + + Error err; +#ifdef ANDROID_ENABLED + err = OS_Android::get_singleton()->sign_apk(apk_path, apk_path, keystore, user, password); + if (err != OK) { + add_message(EXPORT_MESSAGE_ERROR, TTR("Code Signing"), TTR("Unable to sign apk.")); + return err; + } +#else + String target_sdk_version = p_preset->get("gradle_build/target_sdk"); + if (!target_sdk_version.is_valid_int()) { + target_sdk_version = itos(DEFAULT_TARGET_SDK_VERSION); + } + + String apksigner = get_apksigner_path(target_sdk_version.to_int(), true); + print_verbose("Starting signing of the APK binary using " + apksigner); + if (apksigner == "<FAILED>") { + add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("All 'apksigner' tools located in Android SDK 'build-tools' directory failed to execute. Please check that you have the correct version installed for your target sdk version. The resulting APK is unsigned.")); + return OK; + } + if (!FileAccess::exists(apksigner)) { + add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("'apksigner' could not be found. Please check that the command is available in the Android SDK build-tools directory. The resulting APK is unsigned.")); + return OK; + } + String output; List<String> args; args.push_back("sign"); @@ -2778,7 +2805,7 @@ Error EditorExportPlatformAndroid::sign_apk(const Ref<EditorExportPreset> &p_pre args.push_back("pass:" + password); args.push_back("--ks-key-alias"); args.push_back(user); - args.push_back(export_path); + args.push_back(apk_path); if (OS::get_singleton()->is_stdout_verbose() && p_debug) { // We only print verbose logs with credentials for debug builds to avoid leaking release keystore credentials. print_verbose("Signing debug binary using: " + String("\n") + apksigner + " " + join_list(args, String(" "))); @@ -2790,7 +2817,7 @@ Error EditorExportPlatformAndroid::sign_apk(const Ref<EditorExportPreset> &p_pre print_line("Signing binary using: " + String("\n") + apksigner + " " + join_list(redacted_args, String(" "))); } int retval; - Error err = OS::get_singleton()->execute(apksigner, args, &output, &retval, true); + err = OS::get_singleton()->execute(apksigner, args, &output, &retval, true); if (err != OK) { add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Could not start apksigner executable.")); return err; @@ -2802,15 +2829,23 @@ Error EditorExportPlatformAndroid::sign_apk(const Ref<EditorExportPreset> &p_pre add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("output: \n%s"), output)); return ERR_CANT_CREATE; } +#endif - if (ep.step(vformat(TTR("Verifying %s..."), export_label), 105)) { + if (ep.step(TTR("Verifying APK..."), 105)) { return ERR_SKIP; } +#ifdef ANDROID_ENABLED + err = OS_Android::get_singleton()->verify_apk(apk_path); + if (err != OK) { + add_message(EXPORT_MESSAGE_ERROR, TTR("Code Signing"), TTR("Unable to verify signed apk.")); + return err; + } +#else args.clear(); args.push_back("verify"); args.push_back("--verbose"); - args.push_back(export_path); + args.push_back(apk_path); if (p_debug) { print_verbose("Verifying signed build using: " + String("\n") + apksigner + " " + join_list(args, String(" "))); } @@ -2823,10 +2858,11 @@ Error EditorExportPlatformAndroid::sign_apk(const Ref<EditorExportPreset> &p_pre } print_verbose(output); if (retval) { - add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("'apksigner' verification of %s failed."), export_label)); + add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("'apksigner' verification of APK failed.")); add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("output: \n%s"), output)); return ERR_CANT_CREATE; } +#endif print_verbose("Successfully completed signing build."); return OK; @@ -2964,13 +3000,13 @@ bool EditorExportPlatformAndroid::_is_clean_build_required(const Ref<EditorExpor return have_plugins_changed || has_build_dir_changed || first_build; } -Error EditorExportPlatformAndroid::export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags) { +Error EditorExportPlatformAndroid::export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, BitField<EditorExportPlatform::DebugFlags> p_flags) { int export_format = int(p_preset->get("gradle_build/export_format")); bool should_sign = p_preset->get("package/signed"); return export_project_helper(p_preset, p_debug, p_path, export_format, should_sign, p_flags); } -Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int export_format, bool should_sign, int p_flags) { +Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int export_format, bool should_sign, BitField<EditorExportPlatform::DebugFlags> p_flags) { ExportNotifier notifier(*this, p_preset, p_debug, p_path, p_flags); const String base_dir = p_path.get_base_dir(); @@ -2986,7 +3022,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP bool use_gradle_build = bool(p_preset->get("gradle_build/use_gradle_build")); String gradle_build_directory = use_gradle_build ? ExportTemplateManager::get_android_build_directory(p_preset) : ""; - bool p_give_internet = p_flags & (DEBUG_FLAG_DUMB_CLIENT | DEBUG_FLAG_REMOTE_DEBUG); + bool p_give_internet = p_flags.has_flag(DEBUG_FLAG_DUMB_CLIENT) || p_flags.has_flag(DEBUG_FLAG_REMOTE_DEBUG); bool apk_expansion = p_preset->get("apk_expansion/enable"); Vector<ABI> enabled_abis = get_enabled_abis(p_preset); @@ -3091,7 +3127,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP user_data.assets_directory = assets_directory; user_data.libs_directory = gradle_build_directory.path_join("libs"); user_data.debug = p_debug; - if (p_flags & DEBUG_FLAG_DUMB_CLIENT) { + if (p_flags.has_flag(DEBUG_FLAG_DUMB_CLIENT)) { err = export_project_files(p_preset, p_debug, ignore_apk_file, &user_data, copy_gradle_so); } else { err = export_project_files(p_preset, p_debug, rename_and_store_file_in_gradle_project, &user_data, copy_gradle_so); @@ -3319,7 +3355,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP src_apk = find_export_template("android_release.apk"); } if (src_apk.is_empty()) { - add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Package not found: \"%s\"."), src_apk)); + add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("%s export template not found: \"%s\"."), (p_debug ? "Debug" : "Release"), src_apk)); return ERR_FILE_NOT_FOUND; } } @@ -3464,7 +3500,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP } err = OK; - if (p_flags & DEBUG_FLAG_DUMB_CLIENT) { + if (p_flags.has_flag(DEBUG_FLAG_DUMB_CLIENT)) { APKExportData ed; ed.ep = &ep; ed.apk = unaligned_apk; diff --git a/platform/android/export/export_plugin.h b/platform/android/export/export_plugin.h index 97bbd0c7bc..708288fbf4 100644 --- a/platform/android/export/export_plugin.h +++ b/platform/android/export/export_plugin.h @@ -214,7 +214,7 @@ public: virtual String get_device_architecture(int p_index) const override; - virtual Error run(const Ref<EditorExportPreset> &p_preset, int p_device, int p_debug_flags) override; + virtual Error run(const Ref<EditorExportPreset> &p_preset, int p_device, BitField<EditorExportPlatform::DebugFlags> p_debug_flags) override; virtual Ref<Texture2D> get_run_icon() const override; @@ -242,7 +242,7 @@ public: Error save_apk_expansion_file(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path); - void get_command_line_flags(const Ref<EditorExportPreset> &p_preset, const String &p_path, int p_flags, Vector<uint8_t> &r_command_line_flags); + void get_command_line_flags(const Ref<EditorExportPreset> &p_preset, const String &p_path, BitField<EditorExportPlatform::DebugFlags> p_flags, Vector<uint8_t> &r_command_line_flags); Error sign_apk(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &export_path, EditorProgress &ep); @@ -253,9 +253,9 @@ public: static String join_list(const List<String> &p_parts, const String &p_separator); static String join_abis(const Vector<ABI> &p_parts, const String &p_separator, bool p_use_arch); - virtual Error export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags = 0) override; + virtual Error export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, BitField<EditorExportPlatform::DebugFlags> p_flags = 0) override; - Error export_project_helper(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int export_format, bool should_sign, int p_flags); + Error export_project_helper(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int export_format, bool should_sign, BitField<EditorExportPlatform::DebugFlags> p_flags); virtual void get_platform_features(List<String> *r_features) const override; diff --git a/platform/android/file_access_android.cpp b/platform/android/file_access_android.cpp index ae336d6f9d..59b669eabb 100644 --- a/platform/android/file_access_android.cpp +++ b/platform/android/file_access_android.cpp @@ -113,87 +113,6 @@ bool FileAccessAndroid::eof_reached() const { return eof; } -uint8_t FileAccessAndroid::get_8() const { - if (pos >= len) { - eof = true; - return 0; - } - - uint8_t byte; - AAsset_read(asset, &byte, 1); - pos++; - return byte; -} - -uint16_t FileAccessAndroid::get_16() const { - if (pos >= len) { - eof = true; - return 0; - } - - uint16_t bytes = 0; - int r = AAsset_read(asset, &bytes, 2); - - if (r >= 0) { - pos += r; - if (pos >= len) { - eof = true; - } - } - - if (big_endian) { - bytes = BSWAP16(bytes); - } - - return bytes; -} - -uint32_t FileAccessAndroid::get_32() const { - if (pos >= len) { - eof = true; - return 0; - } - - uint32_t bytes = 0; - int r = AAsset_read(asset, &bytes, 4); - - if (r >= 0) { - pos += r; - if (pos >= len) { - eof = true; - } - } - - if (big_endian) { - bytes = BSWAP32(bytes); - } - - return bytes; -} - -uint64_t FileAccessAndroid::get_64() const { - if (pos >= len) { - eof = true; - return 0; - } - - uint64_t bytes = 0; - int r = AAsset_read(asset, &bytes, 8); - - if (r >= 0) { - pos += r; - if (pos >= len) { - eof = true; - } - } - - if (big_endian) { - bytes = BSWAP64(bytes); - } - - return bytes; -} - uint64_t FileAccessAndroid::get_buffer(uint8_t *p_dst, uint64_t p_length) const { ERR_FAIL_COND_V(!p_dst && p_length > 0, -1); @@ -209,6 +128,7 @@ uint64_t FileAccessAndroid::get_buffer(uint8_t *p_dst, uint64_t p_length) const pos = len; } } + return r; } @@ -220,19 +140,7 @@ void FileAccessAndroid::flush() { ERR_FAIL(); } -void FileAccessAndroid::store_8(uint8_t p_dest) { - ERR_FAIL(); -} - -void FileAccessAndroid::store_16(uint16_t p_dest) { - ERR_FAIL(); -} - -void FileAccessAndroid::store_32(uint32_t p_dest) { - ERR_FAIL(); -} - -void FileAccessAndroid::store_64(uint64_t p_dest) { +void FileAccessAndroid::store_buffer(const uint8_t *p_src, uint64_t p_length) { ERR_FAIL(); } diff --git a/platform/android/file_access_android.h b/platform/android/file_access_android.h index e79daeafb3..3224ab50b9 100644 --- a/platform/android/file_access_android.h +++ b/platform/android/file_access_android.h @@ -68,25 +68,18 @@ public: virtual bool eof_reached() const override; // reading passed EOF virtual Error resize(int64_t p_length) override { return ERR_UNAVAILABLE; } - virtual uint8_t get_8() const override; // get a byte - virtual uint16_t get_16() const override; - virtual uint32_t get_32() const override; - virtual uint64_t get_64() const override; virtual uint64_t get_buffer(uint8_t *p_dst, uint64_t p_length) const override; virtual Error get_error() const override; // get last error virtual void flush() override; - virtual void store_8(uint8_t p_dest) override; // store a byte - virtual void store_16(uint16_t p_dest) override; - virtual void store_32(uint32_t p_dest) override; - virtual void store_64(uint64_t p_dest) override; + virtual void store_buffer(const uint8_t *p_src, uint64_t p_length) override; virtual bool file_exists(const String &p_path) override; // return true if a file exists virtual uint64_t _get_modified_time(const String &p_file) override { return 0; } virtual BitField<FileAccess::UnixPermissionFlags> _get_unix_permissions(const String &p_file) override { return 0; } - virtual Error _set_unix_permissions(const String &p_file, BitField<FileAccess::UnixPermissionFlags> p_permissions) override { return FAILED; } + virtual Error _set_unix_permissions(const String &p_file, BitField<FileAccess::UnixPermissionFlags> p_permissions) override { return ERR_UNAVAILABLE; } virtual bool _get_hidden_attribute(const String &p_file) override { return false; } virtual Error _set_hidden_attribute(const String &p_file, bool p_hidden) override { return ERR_UNAVAILABLE; } diff --git a/platform/android/file_access_filesystem_jandroid.cpp b/platform/android/file_access_filesystem_jandroid.cpp index f28d469d07..8b52a00ed8 100644 --- a/platform/android/file_access_filesystem_jandroid.cpp +++ b/platform/android/file_access_filesystem_jandroid.cpp @@ -77,15 +77,9 @@ Error FileAccessFilesystemJAndroid::open_internal(const String &p_path, int p_mo int res = env->CallIntMethod(file_access_handler, _file_open, js, p_mode_flags); env->DeleteLocalRef(js); - if (res <= 0) { - switch (res) { - case 0: - default: - return ERR_FILE_CANT_OPEN; - - case -2: - return ERR_FILE_NOT_FOUND; - } + if (res < 0) { + // Errors are passed back as their negative value to differentiate from the positive file id. + return static_cast<Error>(-res); } id = res; @@ -175,43 +169,6 @@ void FileAccessFilesystemJAndroid::_set_eof(bool eof) { } } -uint8_t FileAccessFilesystemJAndroid::get_8() const { - ERR_FAIL_COND_V_MSG(!is_open(), 0, "File must be opened before use."); - uint8_t byte; - get_buffer(&byte, 1); - return byte; -} - -uint16_t FileAccessFilesystemJAndroid::get_16() const { - ERR_FAIL_COND_V_MSG(!is_open(), 0, "File must be opened before use."); - uint16_t bytes = 0; - get_buffer(reinterpret_cast<uint8_t *>(&bytes), 2); - if (big_endian) { - bytes = BSWAP16(bytes); - } - return bytes; -} - -uint32_t FileAccessFilesystemJAndroid::get_32() const { - ERR_FAIL_COND_V_MSG(!is_open(), 0, "File must be opened before use."); - uint32_t bytes = 0; - get_buffer(reinterpret_cast<uint8_t *>(&bytes), 4); - if (big_endian) { - bytes = BSWAP32(bytes); - } - return bytes; -} - -uint64_t FileAccessFilesystemJAndroid::get_64() const { - ERR_FAIL_COND_V_MSG(!is_open(), 0, "File must be opened before use."); - uint64_t bytes = 0; - get_buffer(reinterpret_cast<uint8_t *>(&bytes), 8); - if (big_endian) { - bytes = BSWAP64(bytes); - } - return bytes; -} - String FileAccessFilesystemJAndroid::get_line() const { ERR_FAIL_COND_V_MSG(!is_open(), String(), "File must be opened before use."); @@ -277,31 +234,6 @@ uint64_t FileAccessFilesystemJAndroid::get_buffer(uint8_t *p_dst, uint64_t p_len } } -void FileAccessFilesystemJAndroid::store_8(uint8_t p_dest) { - store_buffer(&p_dest, 1); -} - -void FileAccessFilesystemJAndroid::store_16(uint16_t p_dest) { - if (big_endian) { - p_dest = BSWAP16(p_dest); - } - store_buffer(reinterpret_cast<uint8_t *>(&p_dest), 2); -} - -void FileAccessFilesystemJAndroid::store_32(uint32_t p_dest) { - if (big_endian) { - p_dest = BSWAP32(p_dest); - } - store_buffer(reinterpret_cast<uint8_t *>(&p_dest), 4); -} - -void FileAccessFilesystemJAndroid::store_64(uint64_t p_dest) { - if (big_endian) { - p_dest = BSWAP64(p_dest); - } - store_buffer(reinterpret_cast<uint8_t *>(&p_dest), 8); -} - void FileAccessFilesystemJAndroid::store_buffer(const uint8_t *p_src, uint64_t p_length) { if (_file_write) { ERR_FAIL_COND_MSG(!is_open(), "File must be opened before use."); @@ -331,19 +263,7 @@ Error FileAccessFilesystemJAndroid::resize(int64_t p_length) { ERR_FAIL_NULL_V(env, FAILED); ERR_FAIL_COND_V_MSG(!is_open(), FAILED, "File must be opened before use."); int res = env->CallIntMethod(file_access_handler, _file_resize, id, p_length); - switch (res) { - case 0: - return OK; - case -4: - return ERR_INVALID_PARAMETER; - case -3: - return ERR_FILE_CANT_OPEN; - case -2: - return ERR_FILE_NOT_FOUND; - case -1: - default: - return FAILED; - } + return static_cast<Error>(res); } else { return ERR_UNAVAILABLE; } diff --git a/platform/android/file_access_filesystem_jandroid.h b/platform/android/file_access_filesystem_jandroid.h index 6a8fc524b7..1345b72fa6 100644 --- a/platform/android/file_access_filesystem_jandroid.h +++ b/platform/android/file_access_filesystem_jandroid.h @@ -78,20 +78,12 @@ public: virtual bool eof_reached() const override; ///< reading passed EOF virtual Error resize(int64_t p_length) override; - virtual uint8_t get_8() const override; ///< get a byte - virtual uint16_t get_16() const override; - virtual uint32_t get_32() const override; - virtual uint64_t get_64() const override; virtual String get_line() const override; ///< get a line virtual uint64_t get_buffer(uint8_t *p_dst, uint64_t p_length) const override; virtual Error get_error() const override; ///< get last error virtual void flush() override; - virtual void store_8(uint8_t p_dest) override; ///< store a byte - virtual void store_16(uint16_t p_dest) override; - virtual void store_32(uint32_t p_dest) override; - virtual void store_64(uint64_t p_dest) override; virtual void store_buffer(const uint8_t *p_src, uint64_t p_length) override; virtual bool file_exists(const String &p_path) override; ///< return true if a file exists @@ -101,7 +93,7 @@ public: virtual uint64_t _get_modified_time(const String &p_file) override; virtual BitField<FileAccess::UnixPermissionFlags> _get_unix_permissions(const String &p_file) override { return 0; } - virtual Error _set_unix_permissions(const String &p_file, BitField<FileAccess::UnixPermissionFlags> p_permissions) override { return FAILED; } + virtual Error _set_unix_permissions(const String &p_file, BitField<FileAccess::UnixPermissionFlags> p_permissions) override { return ERR_UNAVAILABLE; } virtual bool _get_hidden_attribute(const String &p_file) override { return false; } virtual Error _set_hidden_attribute(const String &p_file, bool p_hidden) override { return ERR_UNAVAILABLE; } diff --git a/platform/android/java/lib/THIRDPARTY.md b/platform/android/java/THIRDPARTY.md index 2496b59263..7807cc55ff 100644 --- a/platform/android/java/lib/THIRDPARTY.md +++ b/platform/android/java/THIRDPARTY.md @@ -3,14 +3,6 @@ This file list third-party libraries used in the Android source folder, with their provenance and, when relevant, modifications made to those files. -## com.android.vending.billing - -- Upstream: https://github.com/googlesamples/android-play-billing/tree/master/TrivialDrive/app/src/main -- Version: git (7a94c69, 2019) -- License: Apache 2.0 - -Overwrite the file `aidl/com/android/vending/billing/IInAppBillingService.aidl`. - ## com.google.android.vending.expansion.downloader - Upstream: https://github.com/google/play-apk-expansion/tree/master/apkx_library @@ -19,10 +11,10 @@ Overwrite the file `aidl/com/android/vending/billing/IInAppBillingService.aidl`. Overwrite all files under: -- `src/com/google/android/vending/expansion/downloader` +- `lib/src/com/google/android/vending/expansion/downloader` Some files have been modified for yet unclear reasons. -See the `patches/com.google.android.vending.expansion.downloader.patch` file. +See the `lib/patches/com.google.android.vending.expansion.downloader.patch` file. ## com.google.android.vending.licensing @@ -32,8 +24,18 @@ See the `patches/com.google.android.vending.expansion.downloader.patch` file. Overwrite all files under: -- `aidl/com/android/vending/licensing` -- `src/com/google/android/vending/licensing` +- `lib/aidl/com/android/vending/licensing` +- `lib/src/com/google/android/vending/licensing` Some files have been modified to silence linter errors or fix downstream issues. -See the `patches/com.google.android.vending.licensing.patch` file. +See the `lib/patches/com.google.android.vending.licensing.patch` file. + +## com.android.apksig + +- Upstream: https://android.googlesource.com/platform/tools/apksig/+/ac5cbb07d87cc342fcf07715857a812305d69888 +- Version: git (ac5cbb07d87cc342fcf07715857a812305d69888, 2024) +- License: Apache 2.0 + +Overwrite all files under: + +- `editor/src/main/java/com/android/apksig` diff --git a/platform/android/java/editor/build.gradle b/platform/android/java/editor/build.gradle index 37f68d295a..b8b4233636 100644 --- a/platform/android/java/editor/build.gradle +++ b/platform/android/java/editor/build.gradle @@ -12,6 +12,7 @@ dependencies { implementation "androidx.window:window:1.3.0" implementation "androidx.core:core-splashscreen:$versions.splashscreenVersion" implementation "androidx.constraintlayout:constraintlayout:2.1.4" + implementation "org.bouncycastle:bcprov-jdk15to18:1.77" } ext { @@ -36,7 +37,7 @@ ext { // Return the keystore file used for signing the release build. getGodotKeystoreFile = { -> def keyStore = System.getenv("GODOT_ANDROID_SIGN_KEYSTORE") - if (keyStore == null) { + if (keyStore == null || keyStore.isEmpty()) { return null } return file(keyStore) diff --git a/platform/android/java/editor/src/main/AndroidManifest.xml b/platform/android/java/editor/src/main/AndroidManifest.xml index c7d14a3f49..a875745860 100644 --- a/platform/android/java/editor/src/main/AndroidManifest.xml +++ b/platform/android/java/editor/src/main/AndroidManifest.xml @@ -42,6 +42,7 @@ android:name=".GodotEditor" android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize|density|keyboard|navigation|screenLayout|uiMode" android:exported="true" + android:icon="@mipmap/icon" android:launchMode="singleTask" android:screenOrientation="userLandscape"> <layout @@ -59,9 +60,11 @@ android:name=".GodotGame" android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize|density|keyboard|navigation|screenLayout|uiMode" android:exported="false" - android:label="@string/godot_project_name_string" + android:icon="@mipmap/ic_play_window" + android:label="@string/godot_game_activity_name" android:launchMode="singleTask" android:process=":GodotGame" + android:supportsPictureInPicture="true" android:screenOrientation="userLandscape"> <layout android:defaultWidth="@dimen/editor_default_window_width" diff --git a/platform/android/java/editor/src/main/assets/keystores/debug.keystore b/platform/android/java/editor/src/main/assets/keystores/debug.keystore Binary files differnew file mode 100644 index 0000000000..3b7a97c8ee --- /dev/null +++ b/platform/android/java/editor/src/main/assets/keystores/debug.keystore diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/ApkSigner.java b/platform/android/java/editor/src/main/java/com/android/apksig/ApkSigner.java new file mode 100644 index 0000000000..49796a389e --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/ApkSigner.java @@ -0,0 +1,1801 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig; + +import static com.android.apksig.Constants.LIBRARY_PAGE_ALIGNMENT_BYTES; +import static com.android.apksig.apk.ApkUtils.SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME; +import static com.android.apksig.internal.apk.v3.V3SchemeConstants.MIN_SDK_WITH_V31_SUPPORT; +import static com.android.apksig.internal.apk.v3.V3SchemeConstants.MIN_SDK_WITH_V3_SUPPORT; + +import com.android.apksig.apk.ApkFormatException; +import com.android.apksig.apk.ApkSigningBlockNotFoundException; +import com.android.apksig.apk.ApkUtils; +import com.android.apksig.apk.MinSdkVersionException; +import com.android.apksig.internal.apk.v3.V3SchemeConstants; +import com.android.apksig.internal.util.AndroidSdkVersion; +import com.android.apksig.internal.util.ByteBufferDataSource; +import com.android.apksig.internal.zip.CentralDirectoryRecord; +import com.android.apksig.internal.zip.EocdRecord; +import com.android.apksig.internal.zip.LocalFileRecord; +import com.android.apksig.internal.zip.ZipUtils; +import com.android.apksig.util.DataSink; +import com.android.apksig.util.DataSinks; +import com.android.apksig.util.DataSource; +import com.android.apksig.util.DataSources; +import com.android.apksig.util.ReadableDataSink; +import com.android.apksig.zip.ZipFormatException; + +import java.io.Closeable; +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.SignatureException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * APK signer. + * + * <p>The signer preserves as much of the input APK as possible. For example, it preserves the order + * of APK entries and preserves their contents, including compressed form and alignment of data. + * + * <p>Use {@link Builder} to obtain instances of this signer. + * + * @see <a href="https://source.android.com/security/apksigning/index.html">Application Signing</a> + */ +public class ApkSigner { + + /** + * Extensible data block/field header ID used for storing information about alignment of + * uncompressed entries as well as for aligning the entries's data. See ZIP appnote.txt section + * 4.5 Extensible data fields. + */ + private static final short ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID = (short) 0xd935; + + /** + * Minimum size (in bytes) of the extensible data block/field used for alignment of uncompressed + * entries. + */ + private static final short ALIGNMENT_ZIP_EXTRA_DATA_FIELD_MIN_SIZE_BYTES = 6; + + private static final short ANDROID_FILE_ALIGNMENT_BYTES = 4096; + + /** Name of the Android manifest ZIP entry in APKs. */ + private static final String ANDROID_MANIFEST_ZIP_ENTRY_NAME = "AndroidManifest.xml"; + + private final List<SignerConfig> mSignerConfigs; + private final SignerConfig mSourceStampSignerConfig; + private final SigningCertificateLineage mSourceStampSigningCertificateLineage; + private final boolean mForceSourceStampOverwrite; + private final boolean mSourceStampTimestampEnabled; + private final Integer mMinSdkVersion; + private final int mRotationMinSdkVersion; + private final boolean mRotationTargetsDevRelease; + private final boolean mV1SigningEnabled; + private final boolean mV2SigningEnabled; + private final boolean mV3SigningEnabled; + private final boolean mV4SigningEnabled; + private final boolean mAlignFileSize; + private final boolean mVerityEnabled; + private final boolean mV4ErrorReportingEnabled; + private final boolean mDebuggableApkPermitted; + private final boolean mOtherSignersSignaturesPreserved; + private final boolean mAlignmentPreserved; + private final int mLibraryPageAlignmentBytes; + private final String mCreatedBy; + + private final ApkSignerEngine mSignerEngine; + + private final File mInputApkFile; + private final DataSource mInputApkDataSource; + + private final File mOutputApkFile; + private final DataSink mOutputApkDataSink; + private final DataSource mOutputApkDataSource; + + private final File mOutputV4File; + + private final SigningCertificateLineage mSigningCertificateLineage; + + private ApkSigner( + List<SignerConfig> signerConfigs, + SignerConfig sourceStampSignerConfig, + SigningCertificateLineage sourceStampSigningCertificateLineage, + boolean forceSourceStampOverwrite, + boolean sourceStampTimestampEnabled, + Integer minSdkVersion, + int rotationMinSdkVersion, + boolean rotationTargetsDevRelease, + boolean v1SigningEnabled, + boolean v2SigningEnabled, + boolean v3SigningEnabled, + boolean v4SigningEnabled, + boolean alignFileSize, + boolean verityEnabled, + boolean v4ErrorReportingEnabled, + boolean debuggableApkPermitted, + boolean otherSignersSignaturesPreserved, + boolean alignmentPreserved, + int libraryPageAlignmentBytes, + String createdBy, + ApkSignerEngine signerEngine, + File inputApkFile, + DataSource inputApkDataSource, + File outputApkFile, + DataSink outputApkDataSink, + DataSource outputApkDataSource, + File outputV4File, + SigningCertificateLineage signingCertificateLineage) { + + mSignerConfigs = signerConfigs; + mSourceStampSignerConfig = sourceStampSignerConfig; + mSourceStampSigningCertificateLineage = sourceStampSigningCertificateLineage; + mForceSourceStampOverwrite = forceSourceStampOverwrite; + mSourceStampTimestampEnabled = sourceStampTimestampEnabled; + mMinSdkVersion = minSdkVersion; + mRotationMinSdkVersion = rotationMinSdkVersion; + mRotationTargetsDevRelease = rotationTargetsDevRelease; + mV1SigningEnabled = v1SigningEnabled; + mV2SigningEnabled = v2SigningEnabled; + mV3SigningEnabled = v3SigningEnabled; + mV4SigningEnabled = v4SigningEnabled; + mAlignFileSize = alignFileSize; + mVerityEnabled = verityEnabled; + mV4ErrorReportingEnabled = v4ErrorReportingEnabled; + mDebuggableApkPermitted = debuggableApkPermitted; + mOtherSignersSignaturesPreserved = otherSignersSignaturesPreserved; + mAlignmentPreserved = alignmentPreserved; + mLibraryPageAlignmentBytes = libraryPageAlignmentBytes; + mCreatedBy = createdBy; + + mSignerEngine = signerEngine; + + mInputApkFile = inputApkFile; + mInputApkDataSource = inputApkDataSource; + + mOutputApkFile = outputApkFile; + mOutputApkDataSink = outputApkDataSink; + mOutputApkDataSource = outputApkDataSource; + + mOutputV4File = outputV4File; + + mSigningCertificateLineage = signingCertificateLineage; + } + + /** + * Signs the input APK and outputs the resulting signed APK. The input APK is not modified. + * + * @throws IOException if an I/O error is encountered while reading or writing the APKs + * @throws ApkFormatException if the input APK is malformed + * @throws NoSuchAlgorithmException if the APK signatures cannot be produced or verified because + * a required cryptographic algorithm implementation is missing + * @throws InvalidKeyException if a signature could not be generated because a signing key is + * not suitable for generating the signature + * @throws SignatureException if an error occurred while generating or verifying a signature + * @throws IllegalStateException if this signer's configuration is missing required information + * or if the signing engine is in an invalid state. + */ + public void sign() + throws IOException, ApkFormatException, NoSuchAlgorithmException, InvalidKeyException, + SignatureException, IllegalStateException { + Closeable in = null; + DataSource inputApk; + try { + if (mInputApkDataSource != null) { + inputApk = mInputApkDataSource; + } else if (mInputApkFile != null) { + RandomAccessFile inputFile = new RandomAccessFile(mInputApkFile, "r"); + in = inputFile; + inputApk = DataSources.asDataSource(inputFile); + } else { + throw new IllegalStateException("Input APK not specified"); + } + + Closeable out = null; + try { + DataSink outputApkOut; + DataSource outputApkIn; + if (mOutputApkDataSink != null) { + outputApkOut = mOutputApkDataSink; + outputApkIn = mOutputApkDataSource; + } else if (mOutputApkFile != null) { + RandomAccessFile outputFile = new RandomAccessFile(mOutputApkFile, "rw"); + out = outputFile; + outputFile.setLength(0); + outputApkOut = DataSinks.asDataSink(outputFile); + outputApkIn = DataSources.asDataSource(outputFile); + } else { + throw new IllegalStateException("Output APK not specified"); + } + + sign(inputApk, outputApkOut, outputApkIn); + } finally { + if (out != null) { + out.close(); + } + } + } finally { + if (in != null) { + in.close(); + } + } + } + + private void sign(DataSource inputApk, DataSink outputApkOut, DataSource outputApkIn) + throws IOException, ApkFormatException, NoSuchAlgorithmException, InvalidKeyException, + SignatureException { + // Step 1. Find input APK's main ZIP sections + ApkUtils.ZipSections inputZipSections; + try { + inputZipSections = ApkUtils.findZipSections(inputApk); + } catch (ZipFormatException e) { + throw new ApkFormatException("Malformed APK: not a ZIP archive", e); + } + long inputApkSigningBlockOffset = -1; + DataSource inputApkSigningBlock = null; + try { + ApkUtils.ApkSigningBlock apkSigningBlockInfo = + ApkUtils.findApkSigningBlock(inputApk, inputZipSections); + inputApkSigningBlockOffset = apkSigningBlockInfo.getStartOffset(); + inputApkSigningBlock = apkSigningBlockInfo.getContents(); + } catch (ApkSigningBlockNotFoundException e) { + // Input APK does not contain an APK Signing Block. That's OK. APKs are not required to + // contain this block. It's only needed if the APK is signed using APK Signature Scheme + // v2 and/or v3. + } + DataSource inputApkLfhSection = + inputApk.slice( + 0, + (inputApkSigningBlockOffset != -1) + ? inputApkSigningBlockOffset + : inputZipSections.getZipCentralDirectoryOffset()); + + // Step 2. Parse the input APK's ZIP Central Directory + ByteBuffer inputCd = getZipCentralDirectory(inputApk, inputZipSections); + List<CentralDirectoryRecord> inputCdRecords = + parseZipCentralDirectory(inputCd, inputZipSections); + + List<Hints.PatternWithRange> pinPatterns = + extractPinPatterns(inputCdRecords, inputApkLfhSection); + List<Hints.ByteRange> pinByteRanges = pinPatterns == null ? null : new ArrayList<>(); + + // Step 3. Obtain a signer engine instance + ApkSignerEngine signerEngine; + if (mSignerEngine != null) { + // Use the provided signer engine + signerEngine = mSignerEngine; + } else { + // Construct a signer engine from the provided parameters + int minSdkVersion; + if (mMinSdkVersion != null) { + // No need to extract minSdkVersion from the APK's AndroidManifest.xml + minSdkVersion = mMinSdkVersion; + } else { + // Need to extract minSdkVersion from the APK's AndroidManifest.xml + minSdkVersion = getMinSdkVersionFromApk(inputCdRecords, inputApkLfhSection); + } + List<DefaultApkSignerEngine.SignerConfig> engineSignerConfigs = + new ArrayList<>(mSignerConfigs.size()); + for (SignerConfig signerConfig : mSignerConfigs) { + DefaultApkSignerEngine.SignerConfig.Builder signerConfigBuilder = + new DefaultApkSignerEngine.SignerConfig.Builder( + signerConfig.getName(), + signerConfig.getPrivateKey(), + signerConfig.getCertificates(), + signerConfig.getDeterministicDsaSigning()); + int signerMinSdkVersion = signerConfig.getMinSdkVersion(); + SigningCertificateLineage signerLineage = + signerConfig.getSigningCertificateLineage(); + if (signerMinSdkVersion > 0) { + signerConfigBuilder.setLineageForMinSdkVersion(signerLineage, + signerMinSdkVersion); + } + engineSignerConfigs.add(signerConfigBuilder.build()); + } + DefaultApkSignerEngine.Builder signerEngineBuilder = + new DefaultApkSignerEngine.Builder(engineSignerConfigs, minSdkVersion) + .setV1SigningEnabled(mV1SigningEnabled) + .setV2SigningEnabled(mV2SigningEnabled) + .setV3SigningEnabled(mV3SigningEnabled) + .setVerityEnabled(mVerityEnabled) + .setDebuggableApkPermitted(mDebuggableApkPermitted) + .setOtherSignersSignaturesPreserved(mOtherSignersSignaturesPreserved) + .setSigningCertificateLineage(mSigningCertificateLineage) + .setMinSdkVersionForRotation(mRotationMinSdkVersion) + .setRotationTargetsDevRelease(mRotationTargetsDevRelease); + if (mCreatedBy != null) { + signerEngineBuilder.setCreatedBy(mCreatedBy); + } + if (mSourceStampSignerConfig != null) { + signerEngineBuilder.setStampSignerConfig( + new DefaultApkSignerEngine.SignerConfig.Builder( + mSourceStampSignerConfig.getName(), + mSourceStampSignerConfig.getPrivateKey(), + mSourceStampSignerConfig.getCertificates(), + mSourceStampSignerConfig.getDeterministicDsaSigning()) + .build()); + signerEngineBuilder.setSourceStampTimestampEnabled(mSourceStampTimestampEnabled); + } + if (mSourceStampSigningCertificateLineage != null) { + signerEngineBuilder.setSourceStampSigningCertificateLineage( + mSourceStampSigningCertificateLineage); + } + signerEngine = signerEngineBuilder.build(); + } + + // Step 4. Provide the signer engine with the input APK's APK Signing Block (if any) + if (inputApkSigningBlock != null) { + signerEngine.inputApkSigningBlock(inputApkSigningBlock); + } + + // Step 5. Iterate over input APK's entries and output the Local File Header + data of those + // entries which need to be output. Entries are iterated in the order in which their Local + // File Header records are stored in the file. This is to achieve better data locality in + // case Central Directory entries are in the wrong order. + List<CentralDirectoryRecord> inputCdRecordsSortedByLfhOffset = + new ArrayList<>(inputCdRecords); + Collections.sort( + inputCdRecordsSortedByLfhOffset, + CentralDirectoryRecord.BY_LOCAL_FILE_HEADER_OFFSET_COMPARATOR); + int lastModifiedDateForNewEntries = -1; + int lastModifiedTimeForNewEntries = -1; + long inputOffset = 0; + long outputOffset = 0; + byte[] sourceStampCertificateDigest = null; + Map<String, CentralDirectoryRecord> outputCdRecordsByName = + new HashMap<>(inputCdRecords.size()); + for (final CentralDirectoryRecord inputCdRecord : inputCdRecordsSortedByLfhOffset) { + String entryName = inputCdRecord.getName(); + if (Hints.PIN_BYTE_RANGE_ZIP_ENTRY_NAME.equals(entryName)) { + continue; // We'll re-add below if needed. + } + if (SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME.equals(entryName)) { + try { + sourceStampCertificateDigest = + LocalFileRecord.getUncompressedData( + inputApkLfhSection, inputCdRecord, inputApkLfhSection.size()); + } catch (ZipFormatException ex) { + throw new ApkFormatException("Bad source stamp entry"); + } + continue; // Existing source stamp is handled below as needed. + } + ApkSignerEngine.InputJarEntryInstructions entryInstructions = + signerEngine.inputJarEntry(entryName); + boolean shouldOutput; + switch (entryInstructions.getOutputPolicy()) { + case OUTPUT: + shouldOutput = true; + break; + case OUTPUT_BY_ENGINE: + case SKIP: + shouldOutput = false; + break; + default: + throw new RuntimeException( + "Unknown output policy: " + entryInstructions.getOutputPolicy()); + } + + long inputLocalFileHeaderStartOffset = inputCdRecord.getLocalFileHeaderOffset(); + if (inputLocalFileHeaderStartOffset > inputOffset) { + // Unprocessed data in input starting at inputOffset and ending and the start of + // this record's LFH. We output this data verbatim because this signer is supposed + // to preserve as much of input as possible. + long chunkSize = inputLocalFileHeaderStartOffset - inputOffset; + inputApkLfhSection.feed(inputOffset, chunkSize, outputApkOut); + outputOffset += chunkSize; + inputOffset = inputLocalFileHeaderStartOffset; + } + LocalFileRecord inputLocalFileRecord; + try { + inputLocalFileRecord = + LocalFileRecord.getRecord( + inputApkLfhSection, inputCdRecord, inputApkLfhSection.size()); + } catch (ZipFormatException e) { + throw new ApkFormatException("Malformed ZIP entry: " + inputCdRecord.getName(), e); + } + inputOffset += inputLocalFileRecord.getSize(); + + ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest = + entryInstructions.getInspectJarEntryRequest(); + if (inspectEntryRequest != null) { + fulfillInspectInputJarEntryRequest( + inputApkLfhSection, inputLocalFileRecord, inspectEntryRequest); + } + + if (shouldOutput) { + // Find the max value of last modified, to be used for new entries added by the + // signer. + int lastModifiedDate = inputCdRecord.getLastModificationDate(); + int lastModifiedTime = inputCdRecord.getLastModificationTime(); + if ((lastModifiedDateForNewEntries == -1) + || (lastModifiedDate > lastModifiedDateForNewEntries) + || ((lastModifiedDate == lastModifiedDateForNewEntries) + && (lastModifiedTime > lastModifiedTimeForNewEntries))) { + lastModifiedDateForNewEntries = lastModifiedDate; + lastModifiedTimeForNewEntries = lastModifiedTime; + } + + inspectEntryRequest = signerEngine.outputJarEntry(entryName); + if (inspectEntryRequest != null) { + fulfillInspectInputJarEntryRequest( + inputApkLfhSection, inputLocalFileRecord, inspectEntryRequest); + } + + // Output entry's Local File Header + data + long outputLocalFileHeaderOffset = outputOffset; + OutputSizeAndDataOffset outputLfrResult = + outputInputJarEntryLfhRecord( + inputApkLfhSection, + inputLocalFileRecord, + outputApkOut, + outputLocalFileHeaderOffset); + outputOffset += outputLfrResult.outputBytes; + long outputDataOffset = + outputLocalFileHeaderOffset + outputLfrResult.dataOffsetBytes; + + if (pinPatterns != null) { + boolean pinFileHeader = false; + for (Hints.PatternWithRange pinPattern : pinPatterns) { + if (pinPattern.matcher(inputCdRecord.getName()).matches()) { + Hints.ByteRange dataRange = + new Hints.ByteRange(outputDataOffset, outputOffset); + Hints.ByteRange pinRange = + pinPattern.ClampToAbsoluteByteRange(dataRange); + if (pinRange != null) { + pinFileHeader = true; + pinByteRanges.add(pinRange); + } + } + } + if (pinFileHeader) { + pinByteRanges.add( + new Hints.ByteRange(outputLocalFileHeaderOffset, outputDataOffset)); + } + } + + // Enqueue entry's Central Directory record for output + CentralDirectoryRecord outputCdRecord; + if (outputLocalFileHeaderOffset == inputLocalFileRecord.getStartOffsetInArchive()) { + outputCdRecord = inputCdRecord; + } else { + outputCdRecord = + inputCdRecord.createWithModifiedLocalFileHeaderOffset( + outputLocalFileHeaderOffset); + } + outputCdRecordsByName.put(entryName, outputCdRecord); + } + } + long inputLfhSectionSize = inputApkLfhSection.size(); + if (inputOffset < inputLfhSectionSize) { + // Unprocessed data in input starting at inputOffset and ending and the end of the input + // APK's LFH section. We output this data verbatim because this signer is supposed + // to preserve as much of input as possible. + long chunkSize = inputLfhSectionSize - inputOffset; + inputApkLfhSection.feed(inputOffset, chunkSize, outputApkOut); + outputOffset += chunkSize; + inputOffset = inputLfhSectionSize; + } + + // Step 6. Sort output APK's Central Directory records in the order in which they should + // appear in the output + List<CentralDirectoryRecord> outputCdRecords = new ArrayList<>(inputCdRecords.size() + 10); + for (CentralDirectoryRecord inputCdRecord : inputCdRecords) { + String entryName = inputCdRecord.getName(); + CentralDirectoryRecord outputCdRecord = outputCdRecordsByName.get(entryName); + if (outputCdRecord != null) { + outputCdRecords.add(outputCdRecord); + } + } + + if (lastModifiedDateForNewEntries == -1) { + lastModifiedDateForNewEntries = 0x3a21; // Jan 1 2009 (DOS) + lastModifiedTimeForNewEntries = 0; + } + + // Step 7. Generate and output SourceStamp certificate hash, if necessary. This may output + // more Local File Header + data entries and add to the list of output Central Directory + // records. + if (signerEngine.isEligibleForSourceStamp()) { + byte[] uncompressedData = signerEngine.generateSourceStampCertificateDigest(); + if (mForceSourceStampOverwrite + || sourceStampCertificateDigest == null + || Arrays.equals(uncompressedData, sourceStampCertificateDigest)) { + outputOffset += + outputDataToOutputApk( + SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME, + uncompressedData, + outputOffset, + outputCdRecords, + lastModifiedTimeForNewEntries, + lastModifiedDateForNewEntries, + outputApkOut); + } else { + throw new ApkFormatException( + String.format( + "Cannot generate SourceStamp. APK contains an existing entry with" + + " the name: %s, and it is different than the provided source" + + " stamp certificate", + SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME)); + } + } + + // Step 7.5. Generate pinlist.meta file if necessary. + // This has to be before the step 8 so that the file is signed. + if (pinByteRanges != null) { + // Covers JAR signature and zip central dir entry. + // The signature files don't have to be pinned, but pinning them isn't that wasteful + // since the total size is small. + pinByteRanges.add(new Hints.ByteRange(outputOffset, Long.MAX_VALUE)); + String entryName = Hints.PIN_BYTE_RANGE_ZIP_ENTRY_NAME; + byte[] uncompressedData = Hints.encodeByteRangeList(pinByteRanges); + + requestOutputEntryInspection(signerEngine, entryName, uncompressedData); + outputOffset += + outputDataToOutputApk( + entryName, + uncompressedData, + outputOffset, + outputCdRecords, + lastModifiedTimeForNewEntries, + lastModifiedDateForNewEntries, + outputApkOut); + } + + // Step 8. Generate and output JAR signatures, if necessary. This may output more Local File + // Header + data entries and add to the list of output Central Directory records. + ApkSignerEngine.OutputJarSignatureRequest outputJarSignatureRequest = + signerEngine.outputJarEntries(); + if (outputJarSignatureRequest != null) { + for (ApkSignerEngine.OutputJarSignatureRequest.JarEntry entry : + outputJarSignatureRequest.getAdditionalJarEntries()) { + String entryName = entry.getName(); + byte[] uncompressedData = entry.getData(); + + requestOutputEntryInspection(signerEngine, entryName, uncompressedData); + outputOffset += + outputDataToOutputApk( + entryName, + uncompressedData, + outputOffset, + outputCdRecords, + lastModifiedTimeForNewEntries, + lastModifiedDateForNewEntries, + outputApkOut); + } + outputJarSignatureRequest.done(); + } + + // Step 9. Construct output ZIP Central Directory in an in-memory buffer + long outputCentralDirSizeBytes = 0; + for (CentralDirectoryRecord record : outputCdRecords) { + outputCentralDirSizeBytes += record.getSize(); + } + if (outputCentralDirSizeBytes > Integer.MAX_VALUE) { + throw new IOException( + "Output ZIP Central Directory too large: " + + outputCentralDirSizeBytes + + " bytes"); + } + ByteBuffer outputCentralDir = ByteBuffer.allocate((int) outputCentralDirSizeBytes); + for (CentralDirectoryRecord record : outputCdRecords) { + record.copyTo(outputCentralDir); + } + outputCentralDir.flip(); + DataSource outputCentralDirDataSource = new ByteBufferDataSource(outputCentralDir); + long outputCentralDirStartOffset = outputOffset; + int outputCentralDirRecordCount = outputCdRecords.size(); + + // Step 10. Construct output ZIP End of Central Directory record in an in-memory buffer + // because it can be adjusted in Step 11 due to signing block. + // - CD offset (it's shifted by signing block) + // - Comments (when the output file needs to be sized 4k-aligned) + ByteBuffer outputEocd = + EocdRecord.createWithModifiedCentralDirectoryInfo( + inputZipSections.getZipEndOfCentralDirectory(), + outputCentralDirRecordCount, + outputCentralDirDataSource.size(), + outputCentralDirStartOffset); + + // Step 11. Generate and output APK Signature Scheme v2 and/or v3 signatures and/or + // SourceStamp signatures, if necessary. + // This may insert an APK Signing Block just before the output's ZIP Central Directory + ApkSignerEngine.OutputApkSigningBlockRequest2 outputApkSigningBlockRequest = + signerEngine.outputZipSections2( + outputApkIn, + outputCentralDirDataSource, + DataSources.asDataSource(outputEocd)); + + if (outputApkSigningBlockRequest != null) { + int padding = outputApkSigningBlockRequest.getPaddingSizeBeforeApkSigningBlock(); + byte[] outputApkSigningBlock = outputApkSigningBlockRequest.getApkSigningBlock(); + outputApkSigningBlockRequest.done(); + + long fileSize = + outputCentralDirStartOffset + + outputCentralDirDataSource.size() + + padding + + outputApkSigningBlock.length + + outputEocd.remaining(); + if (mAlignFileSize && (fileSize % ANDROID_FILE_ALIGNMENT_BYTES != 0)) { + int eocdPadding = + (int) + (ANDROID_FILE_ALIGNMENT_BYTES + - fileSize % ANDROID_FILE_ALIGNMENT_BYTES); + // Replace EOCD with padding one so that output file size can be the multiples of + // alignment. + outputEocd = EocdRecord.createWithPaddedComment(outputEocd, eocdPadding); + + // Since EoCD has changed, we need to regenerate signing block as well. + outputApkSigningBlockRequest = + signerEngine.outputZipSections2( + outputApkIn, + new ByteBufferDataSource(outputCentralDir), + DataSources.asDataSource(outputEocd)); + outputApkSigningBlock = outputApkSigningBlockRequest.getApkSigningBlock(); + outputApkSigningBlockRequest.done(); + } + + outputApkOut.consume(ByteBuffer.allocate(padding)); + outputApkOut.consume(outputApkSigningBlock, 0, outputApkSigningBlock.length); + ZipUtils.setZipEocdCentralDirectoryOffset( + outputEocd, + outputCentralDirStartOffset + padding + outputApkSigningBlock.length); + } + + // Step 12. Output ZIP Central Directory and ZIP End of Central Directory + outputCentralDirDataSource.feed(0, outputCentralDirDataSource.size(), outputApkOut); + outputApkOut.consume(outputEocd); + signerEngine.outputDone(); + + // Step 13. Generate and output APK Signature Scheme v4 signatures, if necessary. + if (mV4SigningEnabled) { + signerEngine.signV4(outputApkIn, mOutputV4File, !mV4ErrorReportingEnabled); + } + } + + private static void requestOutputEntryInspection( + ApkSignerEngine signerEngine, + String entryName, + byte[] uncompressedData) + throws IOException { + ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest = + signerEngine.outputJarEntry(entryName); + if (inspectEntryRequest != null) { + inspectEntryRequest.getDataSink().consume( + uncompressedData, 0, uncompressedData.length); + inspectEntryRequest.done(); + } + } + + private static long outputDataToOutputApk( + String entryName, + byte[] uncompressedData, + long localFileHeaderOffset, + List<CentralDirectoryRecord> outputCdRecords, + int lastModifiedTimeForNewEntries, + int lastModifiedDateForNewEntries, + DataSink outputApkOut) + throws IOException { + ZipUtils.DeflateResult deflateResult = ZipUtils.deflate(ByteBuffer.wrap(uncompressedData)); + byte[] compressedData = deflateResult.output; + long uncompressedDataCrc32 = deflateResult.inputCrc32; + long numOfDataBytes = + LocalFileRecord.outputRecordWithDeflateCompressedData( + entryName, + lastModifiedTimeForNewEntries, + lastModifiedDateForNewEntries, + compressedData, + uncompressedDataCrc32, + uncompressedData.length, + outputApkOut); + outputCdRecords.add( + CentralDirectoryRecord.createWithDeflateCompressedData( + entryName, + lastModifiedTimeForNewEntries, + lastModifiedDateForNewEntries, + uncompressedDataCrc32, + compressedData.length, + uncompressedData.length, + localFileHeaderOffset)); + return numOfDataBytes; + } + + private static void fulfillInspectInputJarEntryRequest( + DataSource lfhSection, + LocalFileRecord localFileRecord, + ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest) + throws IOException, ApkFormatException { + try { + localFileRecord.outputUncompressedData(lfhSection, inspectEntryRequest.getDataSink()); + } catch (ZipFormatException e) { + throw new ApkFormatException("Malformed ZIP entry: " + localFileRecord.getName(), e); + } + inspectEntryRequest.done(); + } + + private static class OutputSizeAndDataOffset { + public long outputBytes; + public long dataOffsetBytes; + + public OutputSizeAndDataOffset(long outputBytes, long dataOffsetBytes) { + this.outputBytes = outputBytes; + this.dataOffsetBytes = dataOffsetBytes; + } + } + + private OutputSizeAndDataOffset outputInputJarEntryLfhRecord( + DataSource inputLfhSection, + LocalFileRecord inputRecord, + DataSink outputLfhSection, + long outputOffset) + throws IOException { + long inputOffset = inputRecord.getStartOffsetInArchive(); + if (inputOffset == outputOffset && mAlignmentPreserved) { + // This record's data will be aligned same as in the input APK. + return new OutputSizeAndDataOffset( + inputRecord.outputRecord(inputLfhSection, outputLfhSection), + inputRecord.getDataStartOffsetInRecord()); + } + int dataAlignmentMultiple = getInputJarEntryDataAlignmentMultiple(inputRecord); + if ((dataAlignmentMultiple <= 1) + || ((inputOffset % dataAlignmentMultiple) == (outputOffset % dataAlignmentMultiple) + && mAlignmentPreserved)) { + // This record's data will be aligned same as in the input APK. + return new OutputSizeAndDataOffset( + inputRecord.outputRecord(inputLfhSection, outputLfhSection), + inputRecord.getDataStartOffsetInRecord()); + } + + long inputDataStartOffset = inputOffset + inputRecord.getDataStartOffsetInRecord(); + if ((inputDataStartOffset % dataAlignmentMultiple) != 0 && mAlignmentPreserved) { + // This record's data is not aligned in the input APK. No need to align it in the + // output. + return new OutputSizeAndDataOffset( + inputRecord.outputRecord(inputLfhSection, outputLfhSection), + inputRecord.getDataStartOffsetInRecord()); + } + + // This record's data needs to be re-aligned in the output. This is achieved using the + // record's extra field. + ByteBuffer aligningExtra = + createExtraFieldToAlignData( + inputRecord.getExtra(), + outputOffset + inputRecord.getExtraFieldStartOffsetInsideRecord(), + dataAlignmentMultiple); + long dataOffset = + (long) inputRecord.getDataStartOffsetInRecord() + + aligningExtra.remaining() + - inputRecord.getExtra().remaining(); + return new OutputSizeAndDataOffset( + inputRecord.outputRecordWithModifiedExtra( + inputLfhSection, aligningExtra, outputLfhSection), + dataOffset); + } + + private int getInputJarEntryDataAlignmentMultiple(LocalFileRecord entry) { + if (entry.isDataCompressed()) { + // Compressed entries don't need to be aligned + return 1; + } + + // Attempt to obtain the alignment multiple from the entry's extra field. + ByteBuffer extra = entry.getExtra(); + if (extra.hasRemaining()) { + extra.order(ByteOrder.LITTLE_ENDIAN); + // FORMAT: sequence of fields. Each field consists of: + // * uint16 ID + // * uint16 size + // * 'size' bytes: payload + while (extra.remaining() >= 4) { + short headerId = extra.getShort(); + int dataSize = ZipUtils.getUnsignedInt16(extra); + if (dataSize > extra.remaining()) { + // Malformed field -- insufficient input remaining + break; + } + if (headerId != ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID) { + // Skip this field + extra.position(extra.position() + dataSize); + continue; + } + // This is APK alignment field. + // FORMAT: + // * uint16 alignment multiple (in bytes) + // * remaining bytes -- padding to achieve alignment of data which starts after + // the extra field + if (dataSize < 2) { + // Malformed + break; + } + return ZipUtils.getUnsignedInt16(extra); + } + } + + // Fall back to filename-based defaults + return (entry.getName().endsWith(".so")) ? mLibraryPageAlignmentBytes : 4; + } + + private static ByteBuffer createExtraFieldToAlignData( + ByteBuffer original, long extraStartOffset, int dataAlignmentMultiple) { + if (dataAlignmentMultiple <= 1) { + return original; + } + + // In the worst case scenario, we'll increase the output size by 6 + dataAlignment - 1. + ByteBuffer result = ByteBuffer.allocate(original.remaining() + 5 + dataAlignmentMultiple); + result.order(ByteOrder.LITTLE_ENDIAN); + + // Step 1. Output all extra fields other than the one which is to do with alignment + // FORMAT: sequence of fields. Each field consists of: + // * uint16 ID + // * uint16 size + // * 'size' bytes: payload + while (original.remaining() >= 4) { + short headerId = original.getShort(); + int dataSize = ZipUtils.getUnsignedInt16(original); + if (dataSize > original.remaining()) { + // Malformed field -- insufficient input remaining + break; + } + if (((headerId == 0) && (dataSize == 0)) + || (headerId == ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID)) { + // Ignore the field if it has to do with the old APK data alignment method (filling + // the extra field with 0x00 bytes) or the new APK data alignment method. + original.position(original.position() + dataSize); + continue; + } + // Copy this field (including header) to the output + original.position(original.position() - 4); + int originalLimit = original.limit(); + original.limit(original.position() + 4 + dataSize); + result.put(original); + original.limit(originalLimit); + } + + // Step 2. Add alignment field + // FORMAT: + // * uint16 extra header ID + // * uint16 extra data size + // Payload ('data size' bytes) + // * uint16 alignment multiple (in bytes) + // * remaining bytes -- padding to achieve alignment of data which starts after the + // extra field + long dataMinStartOffset = + extraStartOffset + + result.position() + + ALIGNMENT_ZIP_EXTRA_DATA_FIELD_MIN_SIZE_BYTES; + int paddingSizeBytes = + (dataAlignmentMultiple - ((int) (dataMinStartOffset % dataAlignmentMultiple))) + % dataAlignmentMultiple; + result.putShort(ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID); + ZipUtils.putUnsignedInt16(result, 2 + paddingSizeBytes); + ZipUtils.putUnsignedInt16(result, dataAlignmentMultiple); + result.position(result.position() + paddingSizeBytes); + result.flip(); + + return result; + } + + private static ByteBuffer getZipCentralDirectory( + DataSource apk, ApkUtils.ZipSections apkSections) + throws IOException, ApkFormatException { + long cdSizeBytes = apkSections.getZipCentralDirectorySizeBytes(); + if (cdSizeBytes > Integer.MAX_VALUE) { + throw new ApkFormatException("ZIP Central Directory too large: " + cdSizeBytes); + } + long cdOffset = apkSections.getZipCentralDirectoryOffset(); + ByteBuffer cd = apk.getByteBuffer(cdOffset, (int) cdSizeBytes); + cd.order(ByteOrder.LITTLE_ENDIAN); + return cd; + } + + private static List<CentralDirectoryRecord> parseZipCentralDirectory( + ByteBuffer cd, ApkUtils.ZipSections apkSections) throws ApkFormatException { + long cdOffset = apkSections.getZipCentralDirectoryOffset(); + int expectedCdRecordCount = apkSections.getZipCentralDirectoryRecordCount(); + List<CentralDirectoryRecord> cdRecords = new ArrayList<>(expectedCdRecordCount); + Set<String> entryNames = new HashSet<>(expectedCdRecordCount); + for (int i = 0; i < expectedCdRecordCount; i++) { + CentralDirectoryRecord cdRecord; + int offsetInsideCd = cd.position(); + try { + cdRecord = CentralDirectoryRecord.getRecord(cd); + } catch (ZipFormatException e) { + throw new ApkFormatException( + "Malformed ZIP Central Directory record #" + + (i + 1) + + " at file offset " + + (cdOffset + offsetInsideCd), + e); + } + String entryName = cdRecord.getName(); + if (!entryNames.add(entryName)) { + throw new ApkFormatException( + "Multiple ZIP entries with the same name: " + entryName); + } + cdRecords.add(cdRecord); + } + if (cd.hasRemaining()) { + throw new ApkFormatException( + "Unused space at the end of ZIP Central Directory: " + + cd.remaining() + + " bytes starting at file offset " + + (cdOffset + cd.position())); + } + + return cdRecords; + } + + private static CentralDirectoryRecord findCdRecord( + List<CentralDirectoryRecord> cdRecords, String name) { + for (CentralDirectoryRecord cdRecord : cdRecords) { + if (name.equals(cdRecord.getName())) { + return cdRecord; + } + } + return null; + } + + /** + * Returns the contents of the APK's {@code AndroidManifest.xml} or {@code null} if this entry + * is not present in the APK. + */ + static ByteBuffer getAndroidManifestFromApk( + List<CentralDirectoryRecord> cdRecords, DataSource lhfSection) + throws IOException, ApkFormatException, ZipFormatException { + CentralDirectoryRecord androidManifestCdRecord = + findCdRecord(cdRecords, ANDROID_MANIFEST_ZIP_ENTRY_NAME); + if (androidManifestCdRecord == null) { + throw new ApkFormatException("Missing " + ANDROID_MANIFEST_ZIP_ENTRY_NAME); + } + + return ByteBuffer.wrap( + LocalFileRecord.getUncompressedData( + lhfSection, androidManifestCdRecord, lhfSection.size())); + } + + /** + * Return list of pin patterns embedded in the pin pattern asset file. If no such file, return + * {@code null}. + */ + private static List<Hints.PatternWithRange> extractPinPatterns( + List<CentralDirectoryRecord> cdRecords, DataSource lhfSection) + throws IOException, ApkFormatException { + CentralDirectoryRecord pinListCdRecord = + findCdRecord(cdRecords, Hints.PIN_HINT_ASSET_ZIP_ENTRY_NAME); + List<Hints.PatternWithRange> pinPatterns = null; + if (pinListCdRecord != null) { + pinPatterns = new ArrayList<>(); + byte[] patternBlob; + try { + patternBlob = + LocalFileRecord.getUncompressedData( + lhfSection, pinListCdRecord, lhfSection.size()); + } catch (ZipFormatException ex) { + throw new ApkFormatException("Bad " + pinListCdRecord); + } + pinPatterns = Hints.parsePinPatterns(patternBlob); + } + return pinPatterns; + } + + /** + * Returns the minimum Android version (API Level) supported by the provided APK. This is based + * on the {@code android:minSdkVersion} attributes of the APK's {@code AndroidManifest.xml}. + */ + private static int getMinSdkVersionFromApk( + List<CentralDirectoryRecord> cdRecords, DataSource lhfSection) + throws IOException, MinSdkVersionException { + ByteBuffer androidManifest; + try { + androidManifest = getAndroidManifestFromApk(cdRecords, lhfSection); + } catch (ZipFormatException | ApkFormatException e) { + throw new MinSdkVersionException( + "Failed to determine APK's minimum supported Android platform version", e); + } + return ApkUtils.getMinSdkVersionFromBinaryAndroidManifest(androidManifest); + } + + /** + * Configuration of a signer. + * + * <p>Use {@link Builder} to obtain configuration instances. + */ + public static class SignerConfig { + private final String mName; + private final PrivateKey mPrivateKey; + private final List<X509Certificate> mCertificates; + private final boolean mDeterministicDsaSigning; + private final int mMinSdkVersion; + private final SigningCertificateLineage mSigningCertificateLineage; + + private SignerConfig(Builder builder) { + mName = builder.mName; + mPrivateKey = builder.mPrivateKey; + mCertificates = Collections.unmodifiableList(new ArrayList<>(builder.mCertificates)); + mDeterministicDsaSigning = builder.mDeterministicDsaSigning; + mMinSdkVersion = builder.mMinSdkVersion; + mSigningCertificateLineage = builder.mSigningCertificateLineage; + } + + /** Returns the name of this signer. */ + public String getName() { + return mName; + } + + /** Returns the signing key of this signer. */ + public PrivateKey getPrivateKey() { + return mPrivateKey; + } + + /** + * Returns the certificate(s) of this signer. The first certificate's public key corresponds + * to this signer's private key. + */ + public List<X509Certificate> getCertificates() { + return mCertificates; + } + + /** + * If this signer is a DSA signer, whether or not the signing is done deterministically. + */ + public boolean getDeterministicDsaSigning() { + return mDeterministicDsaSigning; + } + + /** Returns the minimum SDK version for which this signer should be used. */ + public int getMinSdkVersion() { + return mMinSdkVersion; + } + + /** Returns the {@link SigningCertificateLineage} for this signer. */ + public SigningCertificateLineage getSigningCertificateLineage() { + return mSigningCertificateLineage; + } + + /** Builder of {@link SignerConfig} instances. */ + public static class Builder { + private final String mName; + private final PrivateKey mPrivateKey; + private final List<X509Certificate> mCertificates; + private final boolean mDeterministicDsaSigning; + + private int mMinSdkVersion; + private SigningCertificateLineage mSigningCertificateLineage; + + /** + * Constructs a new {@code Builder}. + * + * @param name signer's name. The name is reflected in the name of files comprising the + * JAR signature of the APK. + * @param privateKey signing key + * @param certificates list of one or more X.509 certificates. The subject public key of + * the first certificate must correspond to the {@code privateKey}. + */ + public Builder( + String name, + PrivateKey privateKey, + List<X509Certificate> certificates) { + this(name, privateKey, certificates, false); + } + + /** + * Constructs a new {@code Builder}. + * + * @param name signer's name. The name is reflected in the name of files comprising the + * JAR signature of the APK. + * @param privateKey signing key + * @param certificates list of one or more X.509 certificates. The subject public key of + * the first certificate must correspond to the {@code privateKey}. + * @param deterministicDsaSigning When signing using DSA, whether or not the + * deterministic variant (RFC6979) should be used. + */ + public Builder( + String name, + PrivateKey privateKey, + List<X509Certificate> certificates, + boolean deterministicDsaSigning) { + if (name.isEmpty()) { + throw new IllegalArgumentException("Empty name"); + } + mName = name; + mPrivateKey = privateKey; + mCertificates = new ArrayList<>(certificates); + mDeterministicDsaSigning = deterministicDsaSigning; + } + + /** @see #setLineageForMinSdkVersion(SigningCertificateLineage, int) */ + public Builder setMinSdkVersion(int minSdkVersion) { + return setLineageForMinSdkVersion(null, minSdkVersion); + } + + /** + * Sets the specified {@code minSdkVersion} as the minimum Android platform version + * (API level) for which the provided {@code lineage} (where applicable) should be used + * to produce the APK's signature. This method is useful if callers want to specify a + * particular rotated signer or lineage with restricted capabilities for later + * platform releases. + * + * <p><em>Note:</em>>The V1 and V2 signature schemes do not support key rotation and + * signing lineages with capabilities; only an app's original signer(s) can be used for + * the V1 and V2 signature blocks. Because of this, only a value of {@code + * minSdkVersion} >= 28 (Android P) where support for the V3 signature scheme was + * introduced can be specified. + * + * <p><em>Note:</em>Due to limitations with platform targeting in the V3.0 signature + * scheme, specifying a {@code minSdkVersion} value <= 32 (Android Sv2) will result in + * the current {@code SignerConfig} being used in the V3.0 signing block and applied to + * Android P through at least Sv2 (and later depending on the {@code minSdkVersion} for + * subsequent {@code SignerConfig} instances). Because of this, only a single {@code + * SignerConfig} can be instantiated with a minimum SDK version <= 32. + * + * @param lineage the {@code SigningCertificateLineage} to target the specified {@code + * minSdkVersion} + * @param minSdkVersion the minimum SDK version for which this {@code SignerConfig} + * should be used + * @return this {@code Builder} instance + * + * @throws IllegalArgumentException if the provided {@code minSdkVersion} < 28 or the + * certificate provided in the constructor is not in the specified {@code lineage}. + */ + public Builder setLineageForMinSdkVersion(SigningCertificateLineage lineage, + int minSdkVersion) { + if (minSdkVersion < AndroidSdkVersion.P) { + throw new IllegalArgumentException( + "SDK targeted signing config is only supported with the V3 signature " + + "scheme on Android P (SDK version " + + AndroidSdkVersion.P + ") and later"); + } + if (minSdkVersion < MIN_SDK_WITH_V31_SUPPORT) { + minSdkVersion = AndroidSdkVersion.P; + } + mMinSdkVersion = minSdkVersion; + // If a lineage is provided, ensure the signing certificate for this signer is in + // the lineage; in the case of multiple signing certificates, the first is always + // used in the lineage. + if (lineage != null && !lineage.isCertificateInLineage(mCertificates.get(0))) { + throw new IllegalArgumentException( + "The provided lineage does not contain the signing certificate, " + + mCertificates.get(0).getSubjectDN() + + ", for this SignerConfig"); + } + mSigningCertificateLineage = lineage; + return this; + } + + /** + * Returns a new {@code SignerConfig} instance configured based on the configuration of + * this builder. + */ + public SignerConfig build() { + return new SignerConfig(this); + } + } + } + + /** + * Builder of {@link ApkSigner} instances. + * + * <p>The builder requires the following information to construct a working {@code ApkSigner}: + * + * <ul> + * <li>Signer configs or {@link ApkSignerEngine} -- provided in the constructor, + * <li>APK to be signed -- see {@link #setInputApk(File) setInputApk} variants, + * <li>where to store the output signed APK -- see {@link #setOutputApk(File) setOutputApk} + * variants. + * </ul> + */ + public static class Builder { + private final List<SignerConfig> mSignerConfigs; + private SignerConfig mSourceStampSignerConfig; + private SigningCertificateLineage mSourceStampSigningCertificateLineage; + private boolean mForceSourceStampOverwrite = false; + private boolean mSourceStampTimestampEnabled = true; + private boolean mV1SigningEnabled = true; + private boolean mV2SigningEnabled = true; + private boolean mV3SigningEnabled = true; + private boolean mV4SigningEnabled = true; + private boolean mAlignFileSize = false; + private boolean mVerityEnabled = false; + private boolean mV4ErrorReportingEnabled = false; + private boolean mDebuggableApkPermitted = true; + private boolean mOtherSignersSignaturesPreserved; + private boolean mAlignmentPreserved = false; + private int mLibraryPageAlignmentBytes = LIBRARY_PAGE_ALIGNMENT_BYTES; + private String mCreatedBy; + private Integer mMinSdkVersion; + private int mRotationMinSdkVersion = V3SchemeConstants.DEFAULT_ROTATION_MIN_SDK_VERSION; + private boolean mRotationTargetsDevRelease = false; + + private final ApkSignerEngine mSignerEngine; + + private File mInputApkFile; + private DataSource mInputApkDataSource; + + private File mOutputApkFile; + private DataSink mOutputApkDataSink; + private DataSource mOutputApkDataSource; + + private File mOutputV4File; + + private SigningCertificateLineage mSigningCertificateLineage; + + // APK Signature Scheme v3 only supports a single signing certificate, so to move to v3 + // signing by default, but not require prior clients to update to explicitly disable v3 + // signing for multiple signers, we modify the mV3SigningEnabled depending on the provided + // inputs (multiple signers and mSigningCertificateLineage in particular). Maintain two + // extra variables to record whether or not mV3SigningEnabled has been set directly by a + // client and so should override the default behavior. + private boolean mV3SigningExplicitlyDisabled = false; + private boolean mV3SigningExplicitlyEnabled = false; + + /** + * Constructs a new {@code Builder} for an {@code ApkSigner} which signs using the provided + * signer configurations. The resulting signer may be further customized through this + * builder's setters, such as {@link #setMinSdkVersion(int)}, {@link + * #setV1SigningEnabled(boolean)}, {@link #setV2SigningEnabled(boolean)}, {@link + * #setOtherSignersSignaturesPreserved(boolean)}, {@link #setCreatedBy(String)}. + * + * <p>{@link #Builder(ApkSignerEngine)} is an alternative for advanced use cases where more + * control over low-level details of signing is desired. + */ + public Builder(List<SignerConfig> signerConfigs) { + if (signerConfigs.isEmpty()) { + throw new IllegalArgumentException("At least one signer config must be provided"); + } + if (signerConfigs.size() > 1) { + // APK Signature Scheme v3 only supports single signer, unless a + // SigningCertificateLineage is provided, in which case this will be reset to true, + // since we don't yet have a v4 scheme about which to worry + mV3SigningEnabled = false; + } + mSignerConfigs = new ArrayList<>(signerConfigs); + mSignerEngine = null; + } + + /** + * Constructs a new {@code Builder} for an {@code ApkSigner} which signs using the provided + * signing engine. This is meant for advanced use cases where more control is needed over + * the lower-level details of signing. For typical use cases, {@link #Builder(List)} is more + * appropriate. + */ + public Builder(ApkSignerEngine signerEngine) { + if (signerEngine == null) { + throw new NullPointerException("signerEngine == null"); + } + mSignerEngine = signerEngine; + mSignerConfigs = null; + } + + /** Sets the signing configuration of the source stamp to be embedded in the APK. */ + public Builder setSourceStampSignerConfig(SignerConfig sourceStampSignerConfig) { + mSourceStampSignerConfig = sourceStampSignerConfig; + return this; + } + + /** + * Sets the source stamp {@link SigningCertificateLineage}. This structure provides proof of + * signing certificate rotation for certificates previously used to sign source stamps. + */ + public Builder setSourceStampSigningCertificateLineage( + SigningCertificateLineage sourceStampSigningCertificateLineage) { + mSourceStampSigningCertificateLineage = sourceStampSigningCertificateLineage; + return this; + } + + /** + * Sets whether the APK should overwrite existing source stamp, if found. + * + * @param force {@code true} to require the APK to be overwrite existing source stamp + */ + public Builder setForceSourceStampOverwrite(boolean force) { + mForceSourceStampOverwrite = force; + return this; + } + + /** + * Sets whether the source stamp should contain the timestamp attribute with the time + * at which the source stamp was signed. + */ + public Builder setSourceStampTimestampEnabled(boolean value) { + mSourceStampTimestampEnabled = value; + return this; + } + + /** + * Sets the APK to be signed. + * + * @see #setInputApk(DataSource) + */ + public Builder setInputApk(File inputApk) { + if (inputApk == null) { + throw new NullPointerException("inputApk == null"); + } + mInputApkFile = inputApk; + mInputApkDataSource = null; + return this; + } + + /** + * Sets the APK to be signed. + * + * @see #setInputApk(File) + */ + public Builder setInputApk(DataSource inputApk) { + if (inputApk == null) { + throw new NullPointerException("inputApk == null"); + } + mInputApkDataSource = inputApk; + mInputApkFile = null; + return this; + } + + /** + * Sets the location of the output (signed) APK. {@code ApkSigner} will create this file if + * it doesn't exist. + * + * @see #setOutputApk(ReadableDataSink) + * @see #setOutputApk(DataSink, DataSource) + */ + public Builder setOutputApk(File outputApk) { + if (outputApk == null) { + throw new NullPointerException("outputApk == null"); + } + mOutputApkFile = outputApk; + mOutputApkDataSink = null; + mOutputApkDataSource = null; + return this; + } + + /** + * Sets the readable data sink which will receive the output (signed) APK. After signing, + * the contents of the output APK will be available via the {@link DataSource} interface of + * the sink. + * + * <p>This variant of {@code setOutputApk} is useful for avoiding writing the output APK to + * a file. For example, an in-memory data sink, such as {@link + * DataSinks#newInMemoryDataSink()}, could be used instead of a file. + * + * @see #setOutputApk(File) + * @see #setOutputApk(DataSink, DataSource) + */ + public Builder setOutputApk(ReadableDataSink outputApk) { + if (outputApk == null) { + throw new NullPointerException("outputApk == null"); + } + return setOutputApk(outputApk, outputApk); + } + + /** + * Sets the sink which will receive the output (signed) APK. Data received by the {@code + * outputApkOut} sink must be visible through the {@code outputApkIn} data source. + * + * <p>This is an advanced variant of {@link #setOutputApk(ReadableDataSink)}, enabling the + * sink and the source to be different objects. + * + * @see #setOutputApk(ReadableDataSink) + * @see #setOutputApk(File) + */ + public Builder setOutputApk(DataSink outputApkOut, DataSource outputApkIn) { + if (outputApkOut == null) { + throw new NullPointerException("outputApkOut == null"); + } + if (outputApkIn == null) { + throw new NullPointerException("outputApkIn == null"); + } + mOutputApkFile = null; + mOutputApkDataSink = outputApkOut; + mOutputApkDataSource = outputApkIn; + return this; + } + + /** + * Sets the location of the V4 output file. {@code ApkSigner} will create this file if it + * doesn't exist. + */ + public Builder setV4SignatureOutputFile(File v4SignatureOutputFile) { + if (v4SignatureOutputFile == null) { + throw new NullPointerException("v4HashRootOutputFile == null"); + } + mOutputV4File = v4SignatureOutputFile; + return this; + } + + /** + * Sets the minimum Android platform version (API Level) on which APK signatures produced by + * the signer being built must verify. This method is useful for overriding the default + * behavior where the minimum API Level is obtained from the {@code android:minSdkVersion} + * attribute of the APK's {@code AndroidManifest.xml}. + * + * <p><em>Note:</em> This method may result in APK signatures which don't verify on some + * Android platform versions supported by the APK. + * + * <p><em>Note:</em> This method may only be invoked when this builder is not initialized + * with an {@link ApkSignerEngine}. + * + * @throws IllegalStateException if this builder was initialized with an {@link + * ApkSignerEngine} + */ + public Builder setMinSdkVersion(int minSdkVersion) { + checkInitializedWithoutEngine(); + mMinSdkVersion = minSdkVersion; + return this; + } + + /** + * Sets the minimum Android platform version (API Level) for which an APK's rotated signing + * key should be used to produce the APK's signature. The original signing key for the APK + * will be used for all previous platform versions. If a rotated key with signing lineage is + * not provided then this method is a noop. This method is useful for overriding the + * default behavior where Android T is set as the minimum API level for rotation. + * + * <p><em>Note:</em>Specifying a {@code minSdkVersion} value <= 32 (Android Sv2) will result + * in the original V3 signing block being used without platform targeting. + * + * <p><em>Note:</em> This method may only be invoked when this builder is not initialized + * with an {@link ApkSignerEngine}. + * + * @throws IllegalStateException if this builder was initialized with an {@link + * ApkSignerEngine} + */ + public Builder setMinSdkVersionForRotation(int minSdkVersion) { + checkInitializedWithoutEngine(); + // If the provided SDK version does not support v3.1, then use the default SDK version + // with rotation support. + if (minSdkVersion < MIN_SDK_WITH_V31_SUPPORT) { + mRotationMinSdkVersion = MIN_SDK_WITH_V3_SUPPORT; + } else { + mRotationMinSdkVersion = minSdkVersion; + } + return this; + } + + /** + * Sets whether the rotation-min-sdk-version is intended to target a development release; + * this is primarily required after the T SDK is finalized, and an APK needs to target U + * during its development cycle for rotation. + * + * <p>This is only required after the T SDK is finalized since S and earlier releases do + * not know about the V3.1 block ID, but once T is released and work begins on U, U will + * use the SDK version of T during development. Specifying a rotation-min-sdk-version of T's + * SDK version along with setting {@code enabled} to true will allow an APK to use the + * rotated key on a device running U while causing this to be bypassed for T. + * + * <p><em>Note:</em>If the rotation-min-sdk-version is less than or equal to 32 (Android + * Sv2), then the rotated signing key will be used in the v3.0 signing block and this call + * will be a noop. + * + * <p><em>Note:</em> This method may only be invoked when this builder is not initialized + * with an {@link ApkSignerEngine}. + */ + public Builder setRotationTargetsDevRelease(boolean enabled) { + checkInitializedWithoutEngine(); + mRotationTargetsDevRelease = enabled; + return this; + } + + /** + * Sets whether the APK should be signed using JAR signing (aka v1 signature scheme). + * + * <p>By default, whether APK is signed using JAR signing is determined by {@code + * ApkSigner}, based on the platform versions supported by the APK or specified using {@link + * #setMinSdkVersion(int)}. Disabling JAR signing will result in APK signatures which don't + * verify on Android Marshmallow (Android 6.0, API Level 23) and lower. + * + * <p><em>Note:</em> This method may only be invoked when this builder is not initialized + * with an {@link ApkSignerEngine}. + * + * @param enabled {@code true} to require the APK to be signed using JAR signing, {@code + * false} to require the APK to not be signed using JAR signing. + * @throws IllegalStateException if this builder was initialized with an {@link + * ApkSignerEngine} + * @see <a + * href="https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File">JAR + * signing</a> + */ + public Builder setV1SigningEnabled(boolean enabled) { + checkInitializedWithoutEngine(); + mV1SigningEnabled = enabled; + return this; + } + + /** + * Sets whether the APK should be signed using APK Signature Scheme v2 (aka v2 signature + * scheme). + * + * <p>By default, whether APK is signed using APK Signature Scheme v2 is determined by + * {@code ApkSigner} based on the platform versions supported by the APK or specified using + * {@link #setMinSdkVersion(int)}. + * + * <p><em>Note:</em> This method may only be invoked when this builder is not initialized + * with an {@link ApkSignerEngine}. + * + * @param enabled {@code true} to require the APK to be signed using APK Signature Scheme + * v2, {@code false} to require the APK to not be signed using APK Signature Scheme v2. + * @throws IllegalStateException if this builder was initialized with an {@link + * ApkSignerEngine} + * @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature + * Scheme v2</a> + */ + public Builder setV2SigningEnabled(boolean enabled) { + checkInitializedWithoutEngine(); + mV2SigningEnabled = enabled; + return this; + } + + /** + * Sets whether the APK should be signed using APK Signature Scheme v3 (aka v3 signature + * scheme). + * + * <p>By default, whether APK is signed using APK Signature Scheme v3 is determined by + * {@code ApkSigner} based on the platform versions supported by the APK or specified using + * {@link #setMinSdkVersion(int)}. + * + * <p><em>Note:</em> This method may only be invoked when this builder is not initialized + * with an {@link ApkSignerEngine}. + * + * <p><em>Note:</em> APK Signature Scheme v3 only supports a single signing certificate, but + * may take multiple signers mapping to different targeted platform versions. + * + * @param enabled {@code true} to require the APK to be signed using APK Signature Scheme + * v3, {@code false} to require the APK to not be signed using APK Signature Scheme v3. + * @throws IllegalStateException if this builder was initialized with an {@link + * ApkSignerEngine} + */ + public Builder setV3SigningEnabled(boolean enabled) { + checkInitializedWithoutEngine(); + mV3SigningEnabled = enabled; + if (enabled) { + mV3SigningExplicitlyEnabled = true; + } else { + mV3SigningExplicitlyDisabled = true; + } + return this; + } + + /** + * Sets whether the APK should be signed using APK Signature Scheme v4. + * + * <p>V4 signing requires that the APK be v2 or v3 signed. + * + * @param enabled {@code true} to require the APK to be signed using APK Signature Scheme v2 + * or v3 and generate an v4 signature file + */ + public Builder setV4SigningEnabled(boolean enabled) { + checkInitializedWithoutEngine(); + mV4SigningEnabled = enabled; + mV4ErrorReportingEnabled = enabled; + return this; + } + + /** + * Sets whether errors during v4 signing should be reported and halt the signing process. + * + * <p>Error reporting for v4 signing is disabled by default, but will be enabled if the + * caller invokes {@link #setV4SigningEnabled} with a value of true. This method is useful + * for tools that enable v4 signing by default but don't want to fail the signing process if + * the user did not explicitly request the v4 signing. + * + * @param enabled {@code false} to prevent errors encountered during the V4 signing from + * halting the signing process + */ + public Builder setV4ErrorReportingEnabled(boolean enabled) { + checkInitializedWithoutEngine(); + mV4ErrorReportingEnabled = enabled; + return this; + } + + /** + * Sets whether the output APK files should be sized as multiples of 4K. + * + * <p><em>Note:</em> This method may only be invoked when this builder is not initialized + * with an {@link ApkSignerEngine}. + * + * @throws IllegalStateException if this builder was initialized with an {@link + * ApkSignerEngine} + */ + public Builder setAlignFileSize(boolean alignFileSize) { + checkInitializedWithoutEngine(); + mAlignFileSize = alignFileSize; + return this; + } + + /** + * Sets whether to enable the verity signature algorithm for the v2 and v3 signature + * schemes. + * + * @param enabled {@code true} to enable the verity signature algorithm for inclusion in the + * v2 and v3 signature blocks. + */ + public Builder setVerityEnabled(boolean enabled) { + checkInitializedWithoutEngine(); + mVerityEnabled = enabled; + return this; + } + + /** + * Sets whether the APK should be signed even if it is marked as debuggable ({@code + * android:debuggable="true"} in its {@code AndroidManifest.xml}). For backward + * compatibility reasons, the default value of this setting is {@code true}. + * + * <p>It is dangerous to sign debuggable APKs with production/release keys because Android + * platform loosens security checks for such APKs. For example, arbitrary unauthorized code + * may be executed in the context of such an app by anybody with ADB shell access. + * + * <p><em>Note:</em> This method may only be invoked when this builder is not initialized + * with an {@link ApkSignerEngine}. + */ + public Builder setDebuggableApkPermitted(boolean permitted) { + checkInitializedWithoutEngine(); + mDebuggableApkPermitted = permitted; + return this; + } + + /** + * Sets whether signatures produced by signers other than the ones configured in this engine + * should be copied from the input APK to the output APK. + * + * <p>By default, signatures of other signers are omitted from the output APK. + * + * <p><em>Note:</em> This method may only be invoked when this builder is not initialized + * with an {@link ApkSignerEngine}. + * + * @throws IllegalStateException if this builder was initialized with an {@link + * ApkSignerEngine} + */ + public Builder setOtherSignersSignaturesPreserved(boolean preserved) { + checkInitializedWithoutEngine(); + mOtherSignersSignaturesPreserved = preserved; + return this; + } + + /** + * Sets the value of the {@code Created-By} field in JAR signature files. + * + * <p><em>Note:</em> This method may only be invoked when this builder is not initialized + * with an {@link ApkSignerEngine}. + * + * @throws IllegalStateException if this builder was initialized with an {@link + * ApkSignerEngine} + */ + public Builder setCreatedBy(String createdBy) { + checkInitializedWithoutEngine(); + if (createdBy == null) { + throw new NullPointerException(); + } + mCreatedBy = createdBy; + return this; + } + + private void checkInitializedWithoutEngine() { + if (mSignerEngine != null) { + throw new IllegalStateException( + "Operation is not available when builder initialized with an engine"); + } + } + + /** + * Sets the {@link SigningCertificateLineage} to use with the v3 signature scheme. This + * structure provides proof of signing certificate rotation linking {@link SignerConfig} + * objects to previous ones. + */ + public Builder setSigningCertificateLineage( + SigningCertificateLineage signingCertificateLineage) { + if (signingCertificateLineage != null) { + mV3SigningEnabled = true; + mSigningCertificateLineage = signingCertificateLineage; + } + return this; + } + + /** + * Sets whether the existing alignment within the APK should be preserved; the + * default for this setting is false. When this value is false, the value provided to + * {@link #setLibraryPageAlignmentBytes(int)} will be used to page align native library + * files and 4 bytes will be used to align all other uncompressed files. + */ + public Builder setAlignmentPreserved(boolean alignmentPreserved) { + mAlignmentPreserved = alignmentPreserved; + return this; + } + + /** + * Sets the number of bytes to be used to page align native library files in the APK; the + * default for this setting is {@link Constants#LIBRARY_PAGE_ALIGNMENT_BYTES}. + */ + public Builder setLibraryPageAlignmentBytes(int libraryPageAlignmentBytes) { + mLibraryPageAlignmentBytes = libraryPageAlignmentBytes; + return this; + } + + /** + * Returns a new {@code ApkSigner} instance initialized according to the configuration of + * this builder. + */ + public ApkSigner build() { + if (mV3SigningExplicitlyDisabled && mV3SigningExplicitlyEnabled) { + throw new IllegalStateException( + "Builder configured to both enable and disable APK " + + "Signature Scheme v3 signing"); + } + + if (mV3SigningExplicitlyDisabled) { + mV3SigningEnabled = false; + } + + if (mV3SigningExplicitlyEnabled) { + mV3SigningEnabled = true; + } + + // If V4 signing is not explicitly set, and V2/V3 signing is disabled, then V4 signing + // must be disabled as well as it is dependent on V2/V3. + if (mV4SigningEnabled && !mV2SigningEnabled && !mV3SigningEnabled) { + if (!mV4ErrorReportingEnabled) { + mV4SigningEnabled = false; + } else { + throw new IllegalStateException( + "APK Signature Scheme v4 signing requires at least " + + "v2 or v3 signing to be enabled"); + } + } + + // TODO - if v3 signing is enabled, check provided signers and history to see if valid + + return new ApkSigner( + mSignerConfigs, + mSourceStampSignerConfig, + mSourceStampSigningCertificateLineage, + mForceSourceStampOverwrite, + mSourceStampTimestampEnabled, + mMinSdkVersion, + mRotationMinSdkVersion, + mRotationTargetsDevRelease, + mV1SigningEnabled, + mV2SigningEnabled, + mV3SigningEnabled, + mV4SigningEnabled, + mAlignFileSize, + mVerityEnabled, + mV4ErrorReportingEnabled, + mDebuggableApkPermitted, + mOtherSignersSignaturesPreserved, + mAlignmentPreserved, + mLibraryPageAlignmentBytes, + mCreatedBy, + mSignerEngine, + mInputApkFile, + mInputApkDataSource, + mOutputApkFile, + mOutputApkDataSink, + mOutputApkDataSource, + mOutputV4File, + mSigningCertificateLineage); + } + } +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/ApkSignerEngine.java b/platform/android/java/editor/src/main/java/com/android/apksig/ApkSignerEngine.java new file mode 100644 index 0000000000..c79f232707 --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/ApkSignerEngine.java @@ -0,0 +1,550 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig; + +import com.android.apksig.apk.ApkFormatException; +import com.android.apksig.util.DataSink; +import com.android.apksig.util.DataSource; +import com.android.apksig.util.RunnablesExecutor; + +import java.io.Closeable; +import java.io.File; +import java.io.IOException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.SignatureException; +import java.util.List; +import java.util.Set; + +/** + * APK signing logic which is independent of how input and output APKs are stored, parsed, and + * generated. + * + * <p><h3>Operating Model</h3> + * + * The abstract operating model is that there is an input APK which is being signed, thus producing + * an output APK. In reality, there may be just an output APK being built from scratch, or the input + * APK and the output APK may be the same file. Because this engine does not deal with reading and + * writing files, it can handle all of these scenarios. + * + * <p>The engine is stateful and thus cannot be used for signing multiple APKs. However, once + * the engine signed an APK, the engine can be used to re-sign the APK after it has been modified. + * This may be more efficient than signing the APK using a new instance of the engine. See + * <a href="#incremental">Incremental Operation</a>. + * + * <p>In the engine's operating model, a signed APK is produced as follows. + * <ol> + * <li>JAR entries to be signed are output,</li> + * <li>JAR archive is signed using JAR signing, thus adding the so-called v1 signature to the + * output,</li> + * <li>JAR archive is signed using APK Signature Scheme v2, thus adding the so-called v2 signature + * to the output.</li> + * </ol> + * + * <p>The input APK may contain JAR entries which, depending on the engine's configuration, may or + * may not be output (e.g., existing signatures may need to be preserved or stripped) or which the + * engine will overwrite as part of signing. The engine thus offers {@link #inputJarEntry(String)} + * which tells the client whether the input JAR entry needs to be output. This avoids the need for + * the client to hard-code the aspects of APK signing which determine which parts of input must be + * ignored. Similarly, the engine offers {@link #inputApkSigningBlock(DataSource)} to help the + * client avoid dealing with preserving or stripping APK Signature Scheme v2 signature of the input + * APK. + * + * <p>To use the engine to sign an input APK (or a collection of JAR entries), follow these + * steps: + * <ol> + * <li>Obtain a new instance of the engine -- engine instances are stateful and thus cannot be used + * for signing multiple APKs.</li> + * <li>Locate the input APK's APK Signing Block and provide it to + * {@link #inputApkSigningBlock(DataSource)}.</li> + * <li>For each JAR entry in the input APK, invoke {@link #inputJarEntry(String)} to determine + * whether this entry should be output. The engine may request to inspect the entry.</li> + * <li>For each output JAR entry, invoke {@link #outputJarEntry(String)} which may request to + * inspect the entry.</li> + * <li>Once all JAR entries have been output, invoke {@link #outputJarEntries()} which may request + * that additional JAR entries are output. These entries comprise the output APK's JAR + * signature.</li> + * <li>Locate the ZIP Central Directory and ZIP End of Central Directory sections in the output and + * invoke {@link #outputZipSections2(DataSource, DataSource, DataSource)} which may request that + * an APK Signature Block is inserted before the ZIP Central Directory. The block contains the + * output APK's APK Signature Scheme v2 signature.</li> + * <li>Invoke {@link #outputDone()} to signal that the APK was output in full. The engine will + * confirm that the output APK is signed.</li> + * <li>Invoke {@link #close()} to signal that the engine will no longer be used. This lets the + * engine free any resources it no longer needs. + * </ol> + * + * <p>Some invocations of the engine may provide the client with a task to perform. The client is + * expected to perform all requested tasks before proceeding to the next stage of signing. See + * documentation of each method about the deadlines for performing the tasks requested by the + * method. + * + * <p><h3 id="incremental">Incremental Operation</h3></a> + * + * The engine supports incremental operation where a signed APK is produced, then modified and + * re-signed. This may be useful for IDEs, where an app is frequently re-signed after small changes + * by the developer. Re-signing may be more efficient than signing from scratch. + * + * <p>To use the engine in incremental mode, keep notifying the engine of changes to the APK through + * {@link #inputApkSigningBlock(DataSource)}, {@link #inputJarEntry(String)}, + * {@link #inputJarEntryRemoved(String)}, {@link #outputJarEntry(String)}, + * and {@link #outputJarEntryRemoved(String)}, perform the tasks requested by the engine through + * these methods, and, when a new signed APK is desired, run through steps 5 onwards to re-sign the + * APK. + * + * <p><h3>Output-only Operation</h3> + * + * The engine's abstract operating model consists of an input APK and an output APK. However, it is + * possible to use the engine in output-only mode where the engine's {@code input...} methods are + * not invoked. In this mode, the engine has less control over output because it cannot request that + * some JAR entries are not output. Nevertheless, the engine will attempt to make the output APK + * signed and will report an error if cannot do so. + * + * @see <a href="https://source.android.com/security/apksigning/index.html">Application Signing</a> + */ +public interface ApkSignerEngine extends Closeable { + + default void setExecutor(RunnablesExecutor executor) { + throw new UnsupportedOperationException("setExecutor method is not implemented"); + } + + /** + * Initializes the signer engine with the data already present in the apk (if any). There + * might already be data that can be reused if the entries has not been changed. + * + * @param manifestBytes + * @param entryNames + * @return set of entry names which were processed by the engine during the initialization, a + * subset of entryNames + */ + default Set<String> initWith(byte[] manifestBytes, Set<String> entryNames) { + throw new UnsupportedOperationException("initWith method is not implemented"); + } + + /** + * Indicates to this engine that the input APK contains the provided APK Signing Block. The + * block may contain signatures of the input APK, such as APK Signature Scheme v2 signatures. + * + * @param apkSigningBlock APK signing block of the input APK. The provided data source is + * guaranteed to not be used by the engine after this method terminates. + * + * @throws IOException if an I/O error occurs while reading the APK Signing Block + * @throws ApkFormatException if the APK Signing Block is malformed + * @throws IllegalStateException if this engine is closed + */ + void inputApkSigningBlock(DataSource apkSigningBlock) + throws IOException, ApkFormatException, IllegalStateException; + + /** + * Indicates to this engine that the specified JAR entry was encountered in the input APK. + * + * <p>When an input entry is updated/changed, it's OK to not invoke + * {@link #inputJarEntryRemoved(String)} before invoking this method. + * + * @return instructions about how to proceed with this entry + * + * @throws IllegalStateException if this engine is closed + */ + InputJarEntryInstructions inputJarEntry(String entryName) throws IllegalStateException; + + /** + * Indicates to this engine that the specified JAR entry was output. + * + * <p>It is unnecessary to invoke this method for entries added to output by this engine (e.g., + * requested by {@link #outputJarEntries()}) provided the entries were output with exactly the + * data requested by the engine. + * + * <p>When an already output entry is updated/changed, it's OK to not invoke + * {@link #outputJarEntryRemoved(String)} before invoking this method. + * + * @return request to inspect the entry or {@code null} if the engine does not need to inspect + * the entry. The request must be fulfilled before {@link #outputJarEntries()} is + * invoked. + * + * @throws IllegalStateException if this engine is closed + */ + InspectJarEntryRequest outputJarEntry(String entryName) throws IllegalStateException; + + /** + * Indicates to this engine that the specified JAR entry was removed from the input. It's safe + * to invoke this for entries for which {@link #inputJarEntry(String)} hasn't been invoked. + * + * @return output policy of this JAR entry. The policy indicates how this input entry affects + * the output APK. The client of this engine should use this information to determine + * how the removal of this input APK's JAR entry affects the output APK. + * + * @throws IllegalStateException if this engine is closed + */ + InputJarEntryInstructions.OutputPolicy inputJarEntryRemoved(String entryName) + throws IllegalStateException; + + /** + * Indicates to this engine that the specified JAR entry was removed from the output. It's safe + * to invoke this for entries for which {@link #outputJarEntry(String)} hasn't been invoked. + * + * @throws IllegalStateException if this engine is closed + */ + void outputJarEntryRemoved(String entryName) throws IllegalStateException; + + /** + * Indicates to this engine that all JAR entries have been output. + * + * @return request to add JAR signature to the output or {@code null} if there is no need to add + * a JAR signature. The request will contain additional JAR entries to be output. The + * request must be fulfilled before + * {@link #outputZipSections2(DataSource, DataSource, DataSource)} is invoked. + * + * @throws ApkFormatException if the APK is malformed in a way which is preventing this engine + * from producing a valid signature. For example, if the engine uses the provided + * {@code META-INF/MANIFEST.MF} as a template and the file is malformed. + * @throws NoSuchAlgorithmException if a signature could not be generated because a required + * cryptographic algorithm implementation is missing + * @throws InvalidKeyException if a signature could not be generated because a signing key is + * not suitable for generating the signature + * @throws SignatureException if an error occurred while generating a signature + * @throws IllegalStateException if there are unfulfilled requests, such as to inspect some JAR + * entries, or if the engine is closed + */ + OutputJarSignatureRequest outputJarEntries() + throws ApkFormatException, NoSuchAlgorithmException, InvalidKeyException, + SignatureException, IllegalStateException; + + /** + * Indicates to this engine that the ZIP sections comprising the output APK have been output. + * + * <p>The provided data sources are guaranteed to not be used by the engine after this method + * terminates. + * + * @deprecated This is now superseded by {@link #outputZipSections2(DataSource, DataSource, + * DataSource)}. + * + * @param zipEntries the section of ZIP archive containing Local File Header records and data of + * the ZIP entries. In a well-formed archive, this section starts at the start of the + * archive and extends all the way to the ZIP Central Directory. + * @param zipCentralDirectory ZIP Central Directory section + * @param zipEocd ZIP End of Central Directory (EoCD) record + * + * @return request to add an APK Signing Block to the output or {@code null} if the output must + * not contain an APK Signing Block. The request must be fulfilled before + * {@link #outputDone()} is invoked. + * + * @throws IOException if an I/O error occurs while reading the provided ZIP sections + * @throws ApkFormatException if the provided APK is malformed in a way which prevents this + * engine from producing a valid signature. For example, if the APK Signing Block + * provided to the engine is malformed. + * @throws NoSuchAlgorithmException if a signature could not be generated because a required + * cryptographic algorithm implementation is missing + * @throws InvalidKeyException if a signature could not be generated because a signing key is + * not suitable for generating the signature + * @throws SignatureException if an error occurred while generating a signature + * @throws IllegalStateException if there are unfulfilled requests, such as to inspect some JAR + * entries or to output JAR signature, or if the engine is closed + */ + @Deprecated + OutputApkSigningBlockRequest outputZipSections( + DataSource zipEntries, + DataSource zipCentralDirectory, + DataSource zipEocd) + throws IOException, ApkFormatException, NoSuchAlgorithmException, + InvalidKeyException, SignatureException, IllegalStateException; + + /** + * Indicates to this engine that the ZIP sections comprising the output APK have been output. + * + * <p>The provided data sources are guaranteed to not be used by the engine after this method + * terminates. + * + * @param zipEntries the section of ZIP archive containing Local File Header records and data of + * the ZIP entries. In a well-formed archive, this section starts at the start of the + * archive and extends all the way to the ZIP Central Directory. + * @param zipCentralDirectory ZIP Central Directory section + * @param zipEocd ZIP End of Central Directory (EoCD) record + * + * @return request to add an APK Signing Block to the output or {@code null} if the output must + * not contain an APK Signing Block. The request must be fulfilled before + * {@link #outputDone()} is invoked. + * + * @throws IOException if an I/O error occurs while reading the provided ZIP sections + * @throws ApkFormatException if the provided APK is malformed in a way which prevents this + * engine from producing a valid signature. For example, if the APK Signing Block + * provided to the engine is malformed. + * @throws NoSuchAlgorithmException if a signature could not be generated because a required + * cryptographic algorithm implementation is missing + * @throws InvalidKeyException if a signature could not be generated because a signing key is + * not suitable for generating the signature + * @throws SignatureException if an error occurred while generating a signature + * @throws IllegalStateException if there are unfulfilled requests, such as to inspect some JAR + * entries or to output JAR signature, or if the engine is closed + */ + OutputApkSigningBlockRequest2 outputZipSections2( + DataSource zipEntries, + DataSource zipCentralDirectory, + DataSource zipEocd) + throws IOException, ApkFormatException, NoSuchAlgorithmException, + InvalidKeyException, SignatureException, IllegalStateException; + + /** + * Indicates to this engine that the signed APK was output. + * + * <p>This does not change the output APK. The method helps the client confirm that the current + * output is signed. + * + * @throws IllegalStateException if there are unfulfilled requests, such as to inspect some JAR + * entries or to output signatures, or if the engine is closed + */ + void outputDone() throws IllegalStateException; + + /** + * Generates a V4 signature proto and write to output file. + * + * @param data Input data to calculate a verity hash tree and hash root + * @param outputFile To store the serialized V4 Signature. + * @param ignoreFailures Whether any failures will be silently ignored. + * @throws InvalidKeyException if a signature could not be generated because a signing key is + * not suitable for generating the signature + * @throws NoSuchAlgorithmException if a signature could not be generated because a required + * cryptographic algorithm implementation is missing + * @throws SignatureException if an error occurred while generating a signature + * @throws IOException if protobuf fails to be serialized and written to file + */ + void signV4(DataSource data, File outputFile, boolean ignoreFailures) + throws InvalidKeyException, NoSuchAlgorithmException, SignatureException, IOException; + + /** + * Checks if the signing configuration provided to the engine is capable of creating a + * SourceStamp. + */ + default boolean isEligibleForSourceStamp() { + return false; + } + + /** Generates the digest of the certificate used to sign the source stamp. */ + default byte[] generateSourceStampCertificateDigest() throws SignatureException { + return new byte[0]; + } + + /** + * Indicates to this engine that it will no longer be used. Invoking this on an already closed + * engine is OK. + * + * <p>This does not change the output APK. For example, if the output APK is not yet fully + * signed, it will remain so after this method terminates. + */ + @Override + void close(); + + /** + * Instructions about how to handle an input APK's JAR entry. + * + * <p>The instructions indicate whether to output the entry (see {@link #getOutputPolicy()}) and + * may contain a request to inspect the entry (see {@link #getInspectJarEntryRequest()}), in + * which case the request must be fulfilled before {@link ApkSignerEngine#outputJarEntries()} is + * invoked. + */ + public static class InputJarEntryInstructions { + private final OutputPolicy mOutputPolicy; + private final InspectJarEntryRequest mInspectJarEntryRequest; + + /** + * Constructs a new {@code InputJarEntryInstructions} instance with the provided entry + * output policy and without a request to inspect the entry. + */ + public InputJarEntryInstructions(OutputPolicy outputPolicy) { + this(outputPolicy, null); + } + + /** + * Constructs a new {@code InputJarEntryInstructions} instance with the provided entry + * output mode and with the provided request to inspect the entry. + * + * @param inspectJarEntryRequest request to inspect the entry or {@code null} if there's no + * need to inspect the entry. + */ + public InputJarEntryInstructions( + OutputPolicy outputPolicy, + InspectJarEntryRequest inspectJarEntryRequest) { + mOutputPolicy = outputPolicy; + mInspectJarEntryRequest = inspectJarEntryRequest; + } + + /** + * Returns the output policy for this entry. + */ + public OutputPolicy getOutputPolicy() { + return mOutputPolicy; + } + + /** + * Returns the request to inspect the JAR entry or {@code null} if there is no need to + * inspect the entry. + */ + public InspectJarEntryRequest getInspectJarEntryRequest() { + return mInspectJarEntryRequest; + } + + /** + * Output policy for an input APK's JAR entry. + */ + public static enum OutputPolicy { + /** Entry must not be output. */ + SKIP, + + /** Entry should be output. */ + OUTPUT, + + /** Entry will be output by the engine. The client can thus ignore this input entry. */ + OUTPUT_BY_ENGINE, + } + } + + /** + * Request to inspect the specified JAR entry. + * + * <p>The entry's uncompressed data must be provided to the data sink returned by + * {@link #getDataSink()}. Once the entry's data has been provided to the sink, {@link #done()} + * must be invoked. + */ + interface InspectJarEntryRequest { + + /** + * Returns the data sink into which the entry's uncompressed data should be sent. + */ + DataSink getDataSink(); + + /** + * Indicates that entry's data has been provided in full. + */ + void done(); + + /** + * Returns the name of the JAR entry. + */ + String getEntryName(); + } + + /** + * Request to add JAR signature (aka v1 signature) to the output APK. + * + * <p>Entries listed in {@link #getAdditionalJarEntries()} must be added to the output APK after + * which {@link #done()} must be invoked. + */ + interface OutputJarSignatureRequest { + + /** + * Returns JAR entries that must be added to the output APK. + */ + List<JarEntry> getAdditionalJarEntries(); + + /** + * Indicates that the JAR entries contained in this request were added to the output APK. + */ + void done(); + + /** + * JAR entry. + */ + public static class JarEntry { + private final String mName; + private final byte[] mData; + + /** + * Constructs a new {@code JarEntry} with the provided name and data. + * + * @param data uncompressed data of the entry. Changes to this array will not be + * reflected in {@link #getData()}. + */ + public JarEntry(String name, byte[] data) { + mName = name; + mData = data.clone(); + } + + /** + * Returns the name of this ZIP entry. + */ + public String getName() { + return mName; + } + + /** + * Returns the uncompressed data of this JAR entry. + */ + public byte[] getData() { + return mData.clone(); + } + } + } + + /** + * Request to add the specified APK Signing Block to the output APK. APK Signature Scheme v2 + * signature(s) of the APK are contained in this block. + * + * <p>The APK Signing Block returned by {@link #getApkSigningBlock()} must be placed into the + * output APK such that the block is immediately before the ZIP Central Directory, the offset of + * ZIP Central Directory in the ZIP End of Central Directory record must be adjusted + * accordingly, and then {@link #done()} must be invoked. + * + * <p>If the output contains an APK Signing Block, that block must be replaced by the block + * contained in this request. + * + * @deprecated This is now superseded by {@link OutputApkSigningBlockRequest2}. + */ + @Deprecated + interface OutputApkSigningBlockRequest { + + /** + * Returns the APK Signing Block. + */ + byte[] getApkSigningBlock(); + + /** + * Indicates that the APK Signing Block was output as requested. + */ + void done(); + } + + /** + * Request to add the specified APK Signing Block to the output APK. APK Signature Scheme v2 + * signature(s) of the APK are contained in this block. + * + * <p>The APK Signing Block returned by {@link #getApkSigningBlock()} must be placed into the + * output APK such that the block is immediately before the ZIP Central Directory. Immediately + * before the APK Signing Block must be padding consists of the number of 0x00 bytes returned by + * {@link #getPaddingSizeBeforeApkSigningBlock()}. The offset of ZIP Central Directory in the + * ZIP End of Central Directory record must be adjusted accordingly, and then {@link #done()} + * must be invoked. + * + * <p>If the output contains an APK Signing Block, that block must be replaced by the block + * contained in this request. + */ + interface OutputApkSigningBlockRequest2 { + /** + * Returns the APK Signing Block. + */ + byte[] getApkSigningBlock(); + + /** + * Indicates that the APK Signing Block was output as requested. + */ + void done(); + + /** + * Returns the number of 0x00 bytes the caller must place immediately before APK Signing + * Block. + */ + int getPaddingSizeBeforeApkSigningBlock(); + } +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/ApkVerificationIssue.java b/platform/android/java/editor/src/main/java/com/android/apksig/ApkVerificationIssue.java new file mode 100644 index 0000000000..fa2b7aa58c --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/ApkVerificationIssue.java @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig; + +/** + * This class is intended as a lightweight representation of an APK signature verification issue + * where the client does not require the additional textual details provided by a subclass. + */ +public class ApkVerificationIssue { + /* The V2 signer(s) could not be read from the V2 signature block */ + public static final int V2_SIG_MALFORMED_SIGNERS = 1; + /* A V2 signature block exists without any V2 signers */ + public static final int V2_SIG_NO_SIGNERS = 2; + /* Failed to parse a signer's block in the V2 signature block */ + public static final int V2_SIG_MALFORMED_SIGNER = 3; + /* Failed to parse the signer's signature record in the V2 signature block */ + public static final int V2_SIG_MALFORMED_SIGNATURE = 4; + /* The V2 signer contained no signatures */ + public static final int V2_SIG_NO_SIGNATURES = 5; + /* The V2 signer's certificate could not be parsed */ + public static final int V2_SIG_MALFORMED_CERTIFICATE = 6; + /* No signing certificates exist for the V2 signer */ + public static final int V2_SIG_NO_CERTIFICATES = 7; + /* Failed to parse the V2 signer's digest record */ + public static final int V2_SIG_MALFORMED_DIGEST = 8; + /* The V3 signer(s) could not be read from the V3 signature block */ + public static final int V3_SIG_MALFORMED_SIGNERS = 9; + /* A V3 signature block exists without any V3 signers */ + public static final int V3_SIG_NO_SIGNERS = 10; + /* Failed to parse a signer's block in the V3 signature block */ + public static final int V3_SIG_MALFORMED_SIGNER = 11; + /* Failed to parse the signer's signature record in the V3 signature block */ + public static final int V3_SIG_MALFORMED_SIGNATURE = 12; + /* The V3 signer contained no signatures */ + public static final int V3_SIG_NO_SIGNATURES = 13; + /* The V3 signer's certificate could not be parsed */ + public static final int V3_SIG_MALFORMED_CERTIFICATE = 14; + /* No signing certificates exist for the V3 signer */ + public static final int V3_SIG_NO_CERTIFICATES = 15; + /* Failed to parse the V3 signer's digest record */ + public static final int V3_SIG_MALFORMED_DIGEST = 16; + /* The source stamp signer contained no signatures */ + public static final int SOURCE_STAMP_NO_SIGNATURE = 17; + /* The source stamp signer's certificate could not be parsed */ + public static final int SOURCE_STAMP_MALFORMED_CERTIFICATE = 18; + /* The source stamp contains a signature produced using an unknown algorithm */ + public static final int SOURCE_STAMP_UNKNOWN_SIG_ALGORITHM = 19; + /* Failed to parse the signer's signature in the source stamp signature block */ + public static final int SOURCE_STAMP_MALFORMED_SIGNATURE = 20; + /* The source stamp's signature block failed verification */ + public static final int SOURCE_STAMP_DID_NOT_VERIFY = 21; + /* An exception was encountered when verifying the source stamp */ + public static final int SOURCE_STAMP_VERIFY_EXCEPTION = 22; + /* The certificate digest in the APK does not match the expected digest */ + public static final int SOURCE_STAMP_EXPECTED_DIGEST_MISMATCH = 23; + /* + * The APK contains a source stamp signature block without a corresponding stamp certificate + * digest in the APK contents. + */ + public static final int SOURCE_STAMP_SIGNATURE_BLOCK_WITHOUT_CERT_DIGEST = 24; + /* + * The APK does not contain the source stamp certificate digest file nor the source stamp + * signature block. + */ + public static final int SOURCE_STAMP_CERT_DIGEST_AND_SIG_BLOCK_MISSING = 25; + /* + * None of the signatures provided by the source stamp were produced with a known signature + * algorithm. + */ + public static final int SOURCE_STAMP_NO_SUPPORTED_SIGNATURE = 26; + /* + * The source stamp signer's certificate in the signing block does not match the certificate in + * the APK. + */ + public static final int SOURCE_STAMP_CERTIFICATE_MISMATCH_BETWEEN_SIGNATURE_BLOCK_AND_APK = 27; + /* The APK could not be properly parsed due to a ZIP or APK format exception */ + public static final int MALFORMED_APK = 28; + /* An unexpected exception was caught when attempting to verify the APK's signatures */ + public static final int UNEXPECTED_EXCEPTION = 29; + /* The APK contains the certificate digest file but does not contain a stamp signature block */ + public static final int SOURCE_STAMP_SIG_MISSING = 30; + /* Source stamp block contains a malformed attribute. */ + public static final int SOURCE_STAMP_MALFORMED_ATTRIBUTE = 31; + /* Source stamp block contains an unknown attribute. */ + public static final int SOURCE_STAMP_UNKNOWN_ATTRIBUTE = 32; + /** + * Failed to parse the SigningCertificateLineage structure in the source stamp + * attributes section. + */ + public static final int SOURCE_STAMP_MALFORMED_LINEAGE = 33; + /** + * The source stamp certificate does not match the terminal node in the provided + * proof-of-rotation structure describing the stamp certificate history. + */ + public static final int SOURCE_STAMP_POR_CERT_MISMATCH = 34; + /** + * The source stamp SigningCertificateLineage attribute contains a proof-of-rotation record + * with signature(s) that did not verify. + */ + public static final int SOURCE_STAMP_POR_DID_NOT_VERIFY = 35; + /** No V1 / jar signing signature blocks were found in the APK. */ + public static final int JAR_SIG_NO_SIGNATURES = 36; + /** An exception was encountered when parsing the V1 / jar signer in the signature block. */ + public static final int JAR_SIG_PARSE_EXCEPTION = 37; + /** The source stamp timestamp attribute has an invalid value. */ + public static final int SOURCE_STAMP_INVALID_TIMESTAMP = 38; + + private final int mIssueId; + private final String mFormat; + private final Object[] mParams; + + /** + * Constructs a new {@code ApkVerificationIssue} using the provided {@code format} string and + * {@code params}. + */ + public ApkVerificationIssue(String format, Object... params) { + mIssueId = -1; + mFormat = format; + mParams = params; + } + + /** + * Constructs a new {@code ApkVerificationIssue} using the provided {@code issueId} and {@code + * params}. + */ + public ApkVerificationIssue(int issueId, Object... params) { + mIssueId = issueId; + mFormat = null; + mParams = params; + } + + /** + * Returns the numeric ID for this issue. + */ + public int getIssueId() { + return mIssueId; + } + + /** + * Returns the optional parameters for this issue. + */ + public Object[] getParams() { + return mParams; + } + + @Override + public String toString() { + // If this instance was created by a subclass with a format string then return the same + // formatted String as the subclass. + if (mFormat != null) { + return String.format(mFormat, mParams); + } + StringBuilder result = new StringBuilder("mIssueId: ").append(mIssueId); + for (Object param : mParams) { + result.append(", ").append(param.toString()); + } + return result.toString(); + } +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/ApkVerifier.java b/platform/android/java/editor/src/main/java/com/android/apksig/ApkVerifier.java new file mode 100644 index 0000000000..50b3d9f5e2 --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/ApkVerifier.java @@ -0,0 +1,3657 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig; + +import static com.android.apksig.apk.ApkUtils.SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME; +import static com.android.apksig.apk.ApkUtils.computeSha256DigestBytes; +import static com.android.apksig.apk.ApkUtils.getTargetSandboxVersionFromBinaryAndroidManifest; +import static com.android.apksig.apk.ApkUtils.getTargetSdkVersionFromBinaryAndroidManifest; +import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2; +import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3; +import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V31; +import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V31; +import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V4; +import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_JAR_SIGNATURE_SCHEME; +import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_SOURCE_STAMP; +import static com.android.apksig.internal.apk.ApkSigningBlockUtils.toHex; +import static com.android.apksig.internal.apk.v1.V1SchemeConstants.MANIFEST_ENTRY_NAME; +import static com.android.apksig.internal.apk.v3.V3SchemeConstants.MIN_SDK_WITH_V31_SUPPORT; + +import com.android.apksig.ApkVerifier.Result.V2SchemeSignerInfo; +import com.android.apksig.ApkVerifier.Result.V3SchemeSignerInfo; +import com.android.apksig.SigningCertificateLineage.SignerConfig; +import com.android.apksig.apk.ApkFormatException; +import com.android.apksig.apk.ApkUtils; +import com.android.apksig.internal.apk.ApkSigResult; +import com.android.apksig.internal.apk.ApkSignerInfo; +import com.android.apksig.internal.apk.ApkSigningBlockUtils; +import com.android.apksig.internal.apk.ApkSigningBlockUtils.Result.SignerInfo.ContentDigest; +import com.android.apksig.internal.apk.ContentDigestAlgorithm; +import com.android.apksig.internal.apk.SignatureAlgorithm; +import com.android.apksig.internal.apk.SignatureInfo; +import com.android.apksig.internal.apk.SignatureNotFoundException; +import com.android.apksig.internal.apk.stamp.SourceStampConstants; +import com.android.apksig.internal.apk.stamp.V2SourceStampVerifier; +import com.android.apksig.internal.apk.v1.V1SchemeVerifier; +import com.android.apksig.internal.apk.v2.V2SchemeConstants; +import com.android.apksig.internal.apk.v2.V2SchemeVerifier; +import com.android.apksig.internal.apk.v3.V3SchemeConstants; +import com.android.apksig.internal.apk.v3.V3SchemeVerifier; +import com.android.apksig.internal.apk.v4.V4SchemeVerifier; +import com.android.apksig.internal.util.AndroidSdkVersion; +import com.android.apksig.internal.zip.CentralDirectoryRecord; +import com.android.apksig.internal.zip.LocalFileRecord; +import com.android.apksig.util.DataSource; +import com.android.apksig.util.DataSources; +import com.android.apksig.util.RunnablesExecutor; +import com.android.apksig.zip.ZipFormatException; + +import java.io.Closeable; +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.ByteBuffer; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.SignatureException; +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * APK signature verifier which mimics the behavior of the Android platform. + * + * <p>The verifier is designed to closely mimic the behavior of Android platforms. This is to enable + * the verifier to be used for checking whether an APK's signatures are expected to verify on + * Android. + * + * <p>Use {@link Builder} to obtain instances of this verifier. + * + * @see <a href="https://source.android.com/security/apksigning/index.html">Application Signing</a> + */ +public class ApkVerifier { + + private static final Set<Issue> LINEAGE_RELATED_ISSUES = new HashSet<>(Arrays.asList( + Issue.V3_SIG_MALFORMED_LINEAGE, Issue.V3_INCONSISTENT_LINEAGES, + Issue.V3_SIG_POR_DID_NOT_VERIFY, Issue.V3_SIG_POR_CERT_MISMATCH)); + + private static final Map<Integer, String> SUPPORTED_APK_SIG_SCHEME_NAMES = + loadSupportedApkSigSchemeNames(); + + private static Map<Integer, String> loadSupportedApkSigSchemeNames() { + Map<Integer, String> supportedMap = new HashMap<>(2); + supportedMap.put( + ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2, "APK Signature Scheme v2"); + supportedMap.put( + ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3, "APK Signature Scheme v3"); + return supportedMap; + } + + private final File mApkFile; + private final DataSource mApkDataSource; + private final File mV4SignatureFile; + + private final Integer mMinSdkVersion; + private final int mMaxSdkVersion; + + private ApkVerifier( + File apkFile, + DataSource apkDataSource, + File v4SignatureFile, + Integer minSdkVersion, + int maxSdkVersion) { + mApkFile = apkFile; + mApkDataSource = apkDataSource; + mV4SignatureFile = v4SignatureFile; + mMinSdkVersion = minSdkVersion; + mMaxSdkVersion = maxSdkVersion; + } + + /** + * Verifies the APK's signatures and returns the result of verification. The APK can be + * considered verified iff the result's {@link Result#isVerified()} returns {@code true}. + * The verification result also includes errors, warnings, and information about signers such + * as their signing certificates. + * + * <p>Verification succeeds iff the APK's signature is expected to verify on all Android + * platform versions specified via the {@link Builder}. If the APK's signature is expected to + * not verify on any of the specified platform versions, this method returns a result with one + * or more errors and whose {@link Result#isVerified()} returns {@code false}, or this method + * throws an exception. + * + * @throws IOException if an I/O error is encountered while reading the APK + * @throws ApkFormatException if the APK is malformed + * @throws NoSuchAlgorithmException if the APK's signatures cannot be verified because a + * required cryptographic algorithm implementation is missing + * @throws IllegalStateException if this verifier's configuration is missing required + * information. + */ + public Result verify() throws IOException, ApkFormatException, NoSuchAlgorithmException, + IllegalStateException { + Closeable in = null; + try { + DataSource apk; + if (mApkDataSource != null) { + apk = mApkDataSource; + } else if (mApkFile != null) { + RandomAccessFile f = new RandomAccessFile(mApkFile, "r"); + in = f; + apk = DataSources.asDataSource(f, 0, f.length()); + } else { + throw new IllegalStateException("APK not provided"); + } + return verify(apk); + } finally { + if (in != null) { + in.close(); + } + } + } + + /** + * Verifies the APK's signatures and returns the result of verification. The APK can be + * considered verified iff the result's {@link Result#isVerified()} returns {@code true}. + * The verification result also includes errors, warnings, and information about signers. + * + * @param apk APK file contents + * @throws IOException if an I/O error is encountered while reading the APK + * @throws ApkFormatException if the APK is malformed + * @throws NoSuchAlgorithmException if the APK's signatures cannot be verified because a + * required cryptographic algorithm implementation is missing + */ + private Result verify(DataSource apk) + throws IOException, ApkFormatException, NoSuchAlgorithmException { + int maxSdkVersion = mMaxSdkVersion; + + ApkUtils.ZipSections zipSections; + try { + zipSections = ApkUtils.findZipSections(apk); + } catch (ZipFormatException e) { + throw new ApkFormatException("Malformed APK: not a ZIP archive", e); + } + + ByteBuffer androidManifest = null; + + int minSdkVersion = verifyAndGetMinSdkVersion(apk, zipSections); + + Result result = new Result(); + Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeApkContentDigests = + new HashMap<>(); + + // The SUPPORTED_APK_SIG_SCHEME_NAMES contains the mapping from version number to scheme + // name, but the verifiers use this parameter as the schemes supported by the target SDK + // range. Since the code below skips signature verification based on max SDK the mapping of + // supported schemes needs to be modified to ensure the verifiers do not report a stripped + // signature for an SDK range that does not support that signature version. For instance an + // APK with V1, V2, and V3 signatures and a max SDK of O would skip the V3 signature + // verification, but the SUPPORTED_APK_SIG_SCHEME_NAMES contains version 3, so when the V2 + // verification is performed it would see the stripping protection attribute, see that V3 + // is in the list of supported signatures, and report a stripped signature. + Map<Integer, String> supportedSchemeNames = getSupportedSchemeNames(maxSdkVersion); + + // Android N and newer attempts to verify APKs using the APK Signing Block, which can + // include v2 and/or v3 signatures. If none is found, it falls back to JAR signature + // verification. If the signature is found but does not verify, the APK is rejected. + Set<Integer> foundApkSigSchemeIds = new HashSet<>(2); + if (maxSdkVersion >= AndroidSdkVersion.N) { + RunnablesExecutor executor = RunnablesExecutor.SINGLE_THREADED; + // Android T and newer attempts to verify APKs using APK Signature Scheme V3.1. v3.0 + // also includes stripping protection for the minimum SDK version on which the rotated + // signing key should be used. + int rotationMinSdkVersion = 0; + if (maxSdkVersion >= MIN_SDK_WITH_V31_SUPPORT) { + try { + ApkSigningBlockUtils.Result v31Result = new V3SchemeVerifier.Builder(apk, + zipSections, Math.max(minSdkVersion, MIN_SDK_WITH_V31_SUPPORT), + maxSdkVersion) + .setRunnablesExecutor(executor) + .setBlockId(V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID) + .build() + .verify(); + foundApkSigSchemeIds.add(VERSION_APK_SIGNATURE_SCHEME_V31); + rotationMinSdkVersion = v31Result.signers.stream().mapToInt( + signer -> signer.minSdkVersion).min().orElse(0); + result.mergeFrom(v31Result); + signatureSchemeApkContentDigests.put( + VERSION_APK_SIGNATURE_SCHEME_V31, + getApkContentDigestsFromSigningSchemeResult(v31Result)); + } catch (ApkSigningBlockUtils.SignatureNotFoundException ignored) { + // v3.1 signature not required + } + if (result.containsErrors()) { + return result; + } + } + // Android P and newer attempts to verify APKs using APK Signature Scheme v3; since a + // V3.1 block should only be written with a V3.0 block, always perform the V3.0 check + // if the minSdkVersion supports V3.0. + if (maxSdkVersion >= AndroidSdkVersion.P) { + try { + V3SchemeVerifier.Builder builder = new V3SchemeVerifier.Builder(apk, + zipSections, Math.max(minSdkVersion, AndroidSdkVersion.P), + maxSdkVersion) + .setRunnablesExecutor(executor) + .setBlockId(V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID); + if (rotationMinSdkVersion > 0) { + builder.setRotationMinSdkVersion(rotationMinSdkVersion); + } + ApkSigningBlockUtils.Result v3Result = builder.build().verify(); + foundApkSigSchemeIds.add(ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3); + result.mergeFrom(v3Result); + signatureSchemeApkContentDigests.put( + ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3, + getApkContentDigestsFromSigningSchemeResult(v3Result)); + } catch (ApkSigningBlockUtils.SignatureNotFoundException ignored) { + // v3 signature not required unless a v3.1 signature was found as a v3.1 + // signature is intended to support key rotation on T+ with the v3 signature + // containing the original signing key. + if (foundApkSigSchemeIds.contains( + VERSION_APK_SIGNATURE_SCHEME_V31)) { + result.addError(Issue.V31_BLOCK_FOUND_WITHOUT_V3_BLOCK); + } + } + if (result.containsErrors()) { + return result; + } + } + + // Attempt to verify the APK using v2 signing if necessary. Platforms prior to Android P + // ignore APK Signature Scheme v3 signatures and always attempt to verify either JAR or + // APK Signature Scheme v2 signatures. Android P onwards verifies v2 signatures only if + // no APK Signature Scheme v3 (or newer scheme) signatures were found. + if (minSdkVersion < AndroidSdkVersion.P || foundApkSigSchemeIds.isEmpty()) { + try { + ApkSigningBlockUtils.Result v2Result = + V2SchemeVerifier.verify( + executor, + apk, + zipSections, + supportedSchemeNames, + foundApkSigSchemeIds, + Math.max(minSdkVersion, AndroidSdkVersion.N), + maxSdkVersion); + foundApkSigSchemeIds.add(ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2); + result.mergeFrom(v2Result); + signatureSchemeApkContentDigests.put( + ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2, + getApkContentDigestsFromSigningSchemeResult(v2Result)); + } catch (ApkSigningBlockUtils.SignatureNotFoundException ignored) { + // v2 signature not required + } + if (result.containsErrors()) { + return result; + } + } + + // If v4 file is specified, use additional verification on it + if (mV4SignatureFile != null) { + final ApkSigningBlockUtils.Result v4Result = + V4SchemeVerifier.verify(apk, mV4SignatureFile); + foundApkSigSchemeIds.add( + ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V4); + result.mergeFrom(v4Result); + if (result.containsErrors()) { + return result; + } + } + } + + // Android O and newer requires that APKs targeting security sandbox version 2 and higher + // are signed using APK Signature Scheme v2 or newer. + if (maxSdkVersion >= AndroidSdkVersion.O) { + if (androidManifest == null) { + androidManifest = getAndroidManifestFromApk(apk, zipSections); + } + int targetSandboxVersion = + getTargetSandboxVersionFromBinaryAndroidManifest(androidManifest.slice()); + if (targetSandboxVersion > 1) { + if (foundApkSigSchemeIds.isEmpty()) { + result.addError( + Issue.NO_SIG_FOR_TARGET_SANDBOX_VERSION, + targetSandboxVersion); + } + } + } + + List<CentralDirectoryRecord> cdRecords = + V1SchemeVerifier.parseZipCentralDirectory(apk, zipSections); + + // Attempt to verify the APK using JAR signing if necessary. Platforms prior to Android N + // ignore APK Signature Scheme v2 signatures and always attempt to verify JAR signatures. + // Android N onwards verifies JAR signatures only if no APK Signature Scheme v2 (or newer + // scheme) signatures were found. + if ((minSdkVersion < AndroidSdkVersion.N) || (foundApkSigSchemeIds.isEmpty())) { + V1SchemeVerifier.Result v1Result = + V1SchemeVerifier.verify( + apk, + zipSections, + supportedSchemeNames, + foundApkSigSchemeIds, + minSdkVersion, + maxSdkVersion); + result.mergeFrom(v1Result); + signatureSchemeApkContentDigests.put( + ApkSigningBlockUtils.VERSION_JAR_SIGNATURE_SCHEME, + getApkContentDigestFromV1SigningScheme(cdRecords, apk, zipSections)); + } + if (result.containsErrors()) { + return result; + } + + // Verify the SourceStamp, if found in the APK. + try { + CentralDirectoryRecord sourceStampCdRecord = null; + for (CentralDirectoryRecord cdRecord : cdRecords) { + if (SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME.equals( + cdRecord.getName())) { + sourceStampCdRecord = cdRecord; + break; + } + } + // If SourceStamp file is found inside the APK, there must be a SourceStamp + // block in the APK signing block as well. + if (sourceStampCdRecord != null) { + byte[] sourceStampCertificateDigest = + LocalFileRecord.getUncompressedData( + apk, + sourceStampCdRecord, + zipSections.getZipCentralDirectoryOffset()); + ApkSigResult sourceStampResult = + V2SourceStampVerifier.verify( + apk, + zipSections, + sourceStampCertificateDigest, + signatureSchemeApkContentDigests, + Math.max(minSdkVersion, AndroidSdkVersion.R), + maxSdkVersion); + result.mergeFrom(sourceStampResult); + } + } catch (SignatureNotFoundException ignored) { + result.addWarning(Issue.SOURCE_STAMP_SIG_MISSING); + } catch (ZipFormatException e) { + throw new ApkFormatException("Failed to read APK", e); + } + if (result.containsErrors()) { + return result; + } + + // Check whether v1 and v2 scheme signer identifies match, provided both v1 and v2 + // signatures verified. + if ((result.isVerifiedUsingV1Scheme()) && (result.isVerifiedUsingV2Scheme())) { + ArrayList<Result.V1SchemeSignerInfo> v1Signers = + new ArrayList<>(result.getV1SchemeSigners()); + ArrayList<Result.V2SchemeSignerInfo> v2Signers = + new ArrayList<>(result.getV2SchemeSigners()); + ArrayList<ByteArray> v1SignerCerts = new ArrayList<>(); + ArrayList<ByteArray> v2SignerCerts = new ArrayList<>(); + for (Result.V1SchemeSignerInfo signer : v1Signers) { + try { + v1SignerCerts.add(new ByteArray(signer.getCertificate().getEncoded())); + } catch (CertificateEncodingException e) { + throw new IllegalStateException( + "Failed to encode JAR signer " + signer.getName() + " certs", e); + } + } + for (Result.V2SchemeSignerInfo signer : v2Signers) { + try { + v2SignerCerts.add(new ByteArray(signer.getCertificate().getEncoded())); + } catch (CertificateEncodingException e) { + throw new IllegalStateException( + "Failed to encode APK Signature Scheme v2 signer (index: " + + signer.getIndex() + ") certs", + e); + } + } + + for (int i = 0; i < v1SignerCerts.size(); i++) { + ByteArray v1Cert = v1SignerCerts.get(i); + if (!v2SignerCerts.contains(v1Cert)) { + Result.V1SchemeSignerInfo v1Signer = v1Signers.get(i); + v1Signer.addError(Issue.V2_SIG_MISSING); + break; + } + } + for (int i = 0; i < v2SignerCerts.size(); i++) { + ByteArray v2Cert = v2SignerCerts.get(i); + if (!v1SignerCerts.contains(v2Cert)) { + Result.V2SchemeSignerInfo v2Signer = v2Signers.get(i); + v2Signer.addError(Issue.JAR_SIG_MISSING); + break; + } + } + } + + // If there is a v3 scheme signer and an earlier scheme signer, make sure that there is a + // match, or in the event of signing certificate rotation, that the v1/v2 scheme signer + // matches the oldest signing certificate in the provided SigningCertificateLineage + if (result.isVerifiedUsingV3Scheme() + && (result.isVerifiedUsingV1Scheme() || result.isVerifiedUsingV2Scheme())) { + SigningCertificateLineage lineage = result.getSigningCertificateLineage(); + X509Certificate oldSignerCert; + if (result.isVerifiedUsingV1Scheme()) { + List<Result.V1SchemeSignerInfo> v1Signers = result.getV1SchemeSigners(); + if (v1Signers.size() != 1) { + // APK Signature Scheme v3 only supports single-signers, error to sign with + // multiple and then only one + result.addError(Issue.V3_SIG_MULTIPLE_PAST_SIGNERS); + } + oldSignerCert = v1Signers.get(0).mCertChain.get(0); + } else { + List<Result.V2SchemeSignerInfo> v2Signers = result.getV2SchemeSigners(); + if (v2Signers.size() != 1) { + // APK Signature Scheme v3 only supports single-signers, error to sign with + // multiple and then only one + result.addError(Issue.V3_SIG_MULTIPLE_PAST_SIGNERS); + } + oldSignerCert = v2Signers.get(0).mCerts.get(0); + } + if (lineage == null) { + // no signing certificate history with which to contend, just make sure that v3 + // matches previous versions + List<Result.V3SchemeSignerInfo> v3Signers = result.getV3SchemeSigners(); + if (v3Signers.size() != 1) { + // multiple v3 signers should never exist without rotation history, since + // multiple signers implies a different signer for different platform versions + result.addError(Issue.V3_SIG_MULTIPLE_SIGNERS); + } + try { + if (!Arrays.equals(oldSignerCert.getEncoded(), + v3Signers.get(0).mCerts.get(0).getEncoded())) { + result.addError(Issue.V3_SIG_PAST_SIGNERS_MISMATCH); + } + } catch (CertificateEncodingException e) { + // we just go the encoding for the v1/v2 certs above, so must be v3 + throw new RuntimeException( + "Failed to encode APK Signature Scheme v3 signer cert", e); + } + } else { + // we have some signing history, make sure that the root of the history is the same + // as our v1/v2 signer + try { + lineage = lineage.getSubLineage(oldSignerCert); + if (lineage.size() != 1) { + // the v1/v2 signer was found, but not at the root of the lineage + result.addError(Issue.V3_SIG_PAST_SIGNERS_MISMATCH); + } + } catch (IllegalArgumentException e) { + // the v1/v2 signer was not found in the lineage + result.addError(Issue.V3_SIG_PAST_SIGNERS_MISMATCH); + } + } + } + + + // If there is a v4 scheme signer, make sure that their certificates match. + // The apkDigest field in the v4 signature should match the selected v2/v3. + if (result.isVerifiedUsingV4Scheme()) { + List<Result.V4SchemeSignerInfo> v4Signers = result.getV4SchemeSigners(); + + List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> digestsFromV4 = + v4Signers.get(0).getContentDigests(); + if (digestsFromV4.size() != 1) { + result.addError(Issue.V4_SIG_UNEXPECTED_DIGESTS, digestsFromV4.size()); + if (digestsFromV4.isEmpty()) { + return result; + } + } + final byte[] digestFromV4 = digestsFromV4.get(0).getValue(); + + if (result.isVerifiedUsingV3Scheme()) { + final boolean isV31 = result.isVerifiedUsingV31Scheme(); + final int expectedSize = isV31 ? 2 : 1; + if (v4Signers.size() != expectedSize) { + result.addError(isV31 ? Issue.V41_SIG_NEEDS_TWO_SIGNERS + : Issue.V4_SIG_MULTIPLE_SIGNERS); + return result; + } + + checkV4Signer(result.getV3SchemeSigners(), v4Signers.get(0).mCerts, digestFromV4, + result); + if (isV31) { + List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> digestsFromV41 = + v4Signers.get(1).getContentDigests(); + if (digestsFromV41.size() != 1) { + result.addError(Issue.V4_SIG_UNEXPECTED_DIGESTS, digestsFromV41.size()); + if (digestsFromV41.isEmpty()) { + return result; + } + } + final byte[] digestFromV41 = digestsFromV41.get(0).getValue(); + checkV4Signer(result.getV31SchemeSigners(), v4Signers.get(1).mCerts, + digestFromV41, result); + } + } else if (result.isVerifiedUsingV2Scheme()) { + if (v4Signers.size() != 1) { + result.addError(Issue.V4_SIG_MULTIPLE_SIGNERS); + } + + List<Result.V2SchemeSignerInfo> v2Signers = result.getV2SchemeSigners(); + if (v2Signers.size() != 1) { + result.addError(Issue.V4_SIG_MULTIPLE_SIGNERS); + } + + // Compare certificates. + checkV4Certificate(v4Signers.get(0).mCerts, v2Signers.get(0).mCerts, result); + + // Compare digests. + final byte[] digestFromV2 = pickBestDigestForV4( + v2Signers.get(0).getContentDigests()); + if (!Arrays.equals(digestFromV4, digestFromV2)) { + result.addError(Issue.V4_SIG_V2_V3_DIGESTS_MISMATCH, 2, toHex(digestFromV2), + toHex(digestFromV4)); + } + } else { + throw new RuntimeException("V4 signature must be also verified with V2/V3"); + } + } + + // If the targetSdkVersion has a minimum required signature scheme version then verify + // that the APK was signed with at least that version. + try { + if (androidManifest == null) { + androidManifest = getAndroidManifestFromApk(apk, zipSections); + } + } catch (ApkFormatException e) { + // If the manifest is not available then skip the minimum signature scheme requirement + // to support bundle verification. + } + if (androidManifest != null) { + int targetSdkVersion = getTargetSdkVersionFromBinaryAndroidManifest( + androidManifest.slice()); + int minSchemeVersion = getMinimumSignatureSchemeVersionForTargetSdk(targetSdkVersion); + // The platform currently only enforces a single minimum signature scheme version, but + // when later platform versions support another minimum version this will need to be + // expanded to verify the minimum based on the target and maximum SDK version. + if (minSchemeVersion > VERSION_JAR_SIGNATURE_SCHEME + && maxSdkVersion >= targetSdkVersion) { + switch (minSchemeVersion) { + case VERSION_APK_SIGNATURE_SCHEME_V2: + if (result.isVerifiedUsingV2Scheme()) { + break; + } + // Allow this case to fall through to the next as a signature satisfying a + // later scheme version will also satisfy this requirement. + case VERSION_APK_SIGNATURE_SCHEME_V3: + if (result.isVerifiedUsingV3Scheme() || result.isVerifiedUsingV31Scheme()) { + break; + } + result.addError(Issue.MIN_SIG_SCHEME_FOR_TARGET_SDK_NOT_MET, + targetSdkVersion, + minSchemeVersion); + } + } + } + + if (result.containsErrors()) { + return result; + } + + // Verified + result.setVerified(); + if (result.isVerifiedUsingV31Scheme()) { + List<Result.V3SchemeSignerInfo> v31Signers = result.getV31SchemeSigners(); + result.addSignerCertificate(v31Signers.get(v31Signers.size() - 1).getCertificate()); + } else if (result.isVerifiedUsingV3Scheme()) { + List<Result.V3SchemeSignerInfo> v3Signers = result.getV3SchemeSigners(); + result.addSignerCertificate(v3Signers.get(v3Signers.size() - 1).getCertificate()); + } else if (result.isVerifiedUsingV2Scheme()) { + for (Result.V2SchemeSignerInfo signerInfo : result.getV2SchemeSigners()) { + result.addSignerCertificate(signerInfo.getCertificate()); + } + } else if (result.isVerifiedUsingV1Scheme()) { + for (Result.V1SchemeSignerInfo signerInfo : result.getV1SchemeSigners()) { + result.addSignerCertificate(signerInfo.getCertificate()); + } + } else { + throw new RuntimeException( + "APK verified, but has not verified using any of v1, v2 or v3 schemes"); + } + + return result; + } + + /** + * Verifies and returns the minimum SDK version, either as provided to the builder or as read + * from the {@code apk}'s AndroidManifest.xml. + */ + private int verifyAndGetMinSdkVersion(DataSource apk, ApkUtils.ZipSections zipSections) + throws ApkFormatException, IOException { + if (mMinSdkVersion != null) { + if (mMinSdkVersion < 0) { + throw new IllegalArgumentException( + "minSdkVersion must not be negative: " + mMinSdkVersion); + } + if ((mMinSdkVersion != null) && (mMinSdkVersion > mMaxSdkVersion)) { + throw new IllegalArgumentException( + "minSdkVersion (" + mMinSdkVersion + ") > maxSdkVersion (" + mMaxSdkVersion + + ")"); + } + return mMinSdkVersion; + } + + ByteBuffer androidManifest = null; + // Need to obtain minSdkVersion from the APK's AndroidManifest.xml + if (androidManifest == null) { + androidManifest = getAndroidManifestFromApk(apk, zipSections); + } + int minSdkVersion = + ApkUtils.getMinSdkVersionFromBinaryAndroidManifest(androidManifest.slice()); + if (minSdkVersion > mMaxSdkVersion) { + throw new IllegalArgumentException( + "minSdkVersion from APK (" + minSdkVersion + ") > maxSdkVersion (" + + mMaxSdkVersion + ")"); + } + return minSdkVersion; + } + + /** + * Returns the mapping of signature scheme version to signature scheme name for all signature + * schemes starting from V2 supported by the {@code maxSdkVersion}. + */ + private static Map<Integer, String> getSupportedSchemeNames(int maxSdkVersion) { + Map<Integer, String> supportedSchemeNames; + if (maxSdkVersion >= AndroidSdkVersion.P) { + supportedSchemeNames = SUPPORTED_APK_SIG_SCHEME_NAMES; + } else if (maxSdkVersion >= AndroidSdkVersion.N) { + supportedSchemeNames = new HashMap<>(1); + supportedSchemeNames.put(ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2, + SUPPORTED_APK_SIG_SCHEME_NAMES.get( + ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2)); + } else { + supportedSchemeNames = Collections.emptyMap(); + } + return supportedSchemeNames; + } + + /** + * Verifies the APK's source stamp signature and returns the result of the verification. + * + * <p>The APK's source stamp can be considered verified if the result's {@link + * Result#isVerified} returns {@code true}. The details of the source stamp verification can + * be obtained from the result's {@link Result#getSourceStampInfo()}} including the success or + * failure cause from {@link Result.SourceStampInfo#getSourceStampVerificationStatus()}. If the + * verification fails additional details regarding the failure can be obtained from {@link + * Result#getAllErrors()}}. + */ + public Result verifySourceStamp() { + return verifySourceStamp(null); + } + + /** + * Verifies the APK's source stamp signature, including verification that the SHA-256 digest of + * the stamp signing certificate matches the {@code expectedCertDigest}, and returns the result + * of the verification. + * + * <p>A value of {@code null} for the {@code expectedCertDigest} will verify the source stamp, + * if present, without verifying the actual source stamp certificate used to sign the source + * stamp. This can be used to verify an APK contains a properly signed source stamp without + * verifying a particular signer. + * + * @see #verifySourceStamp() + */ + public Result verifySourceStamp(String expectedCertDigest) { + Closeable in = null; + try { + DataSource apk; + if (mApkDataSource != null) { + apk = mApkDataSource; + } else if (mApkFile != null) { + RandomAccessFile f = new RandomAccessFile(mApkFile, "r"); + in = f; + apk = DataSources.asDataSource(f, 0, f.length()); + } else { + throw new IllegalStateException("APK not provided"); + } + return verifySourceStamp(apk, expectedCertDigest); + } catch (IOException e) { + return createSourceStampResultWithError( + Result.SourceStampInfo.SourceStampVerificationStatus.VERIFICATION_ERROR, + Issue.UNEXPECTED_EXCEPTION, e); + } finally { + if (in != null) { + try { + in.close(); + } catch (IOException ignored) { + } + } + } + } + + /** + * Compares the digests coming from signature blocks. Returns {@code true} if at least one + * digest algorithm is present in both digests and actual digests for all common algorithms + * are the same. + */ + public static boolean compareDigests( + Map<ContentDigestAlgorithm, byte[]> firstDigests, + Map<ContentDigestAlgorithm, byte[]> secondDigests) throws NoSuchAlgorithmException { + + Set<ContentDigestAlgorithm> intersectKeys = new HashSet<>(firstDigests.keySet()); + intersectKeys.retainAll(secondDigests.keySet()); + if (intersectKeys.isEmpty()) { + return false; + } + + for (ContentDigestAlgorithm algorithm : intersectKeys) { + if (!Arrays.equals(firstDigests.get(algorithm), + secondDigests.get(algorithm))) { + return false; + } + } + return true; + } + + + /** + * Verifies the provided {@code apk}'s source stamp signature, including verification of the + * SHA-256 digest of the stamp signing certificate matches the {@code expectedCertDigest}, and + * returns the result of the verification. + * + * @see #verifySourceStamp(String) + */ + private Result verifySourceStamp(DataSource apk, String expectedCertDigest) { + try { + ApkUtils.ZipSections zipSections = ApkUtils.findZipSections(apk); + int minSdkVersion = verifyAndGetMinSdkVersion(apk, zipSections); + + // Attempt to obtain the source stamp's certificate digest from the APK. + List<CentralDirectoryRecord> cdRecords = + V1SchemeVerifier.parseZipCentralDirectory(apk, zipSections); + CentralDirectoryRecord sourceStampCdRecord = null; + for (CentralDirectoryRecord cdRecord : cdRecords) { + if (SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME.equals(cdRecord.getName())) { + sourceStampCdRecord = cdRecord; + break; + } + } + + // If the source stamp's certificate digest is not available within the APK then the + // source stamp cannot be verified; check if a source stamp signing block is in the + // APK's signature block to determine the appropriate status to return. + if (sourceStampCdRecord == null) { + boolean stampSigningBlockFound; + try { + ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result( + VERSION_SOURCE_STAMP); + ApkSigningBlockUtils.findSignature(apk, zipSections, + SourceStampConstants.V2_SOURCE_STAMP_BLOCK_ID, result); + stampSigningBlockFound = true; + } catch (ApkSigningBlockUtils.SignatureNotFoundException e) { + stampSigningBlockFound = false; + } + if (stampSigningBlockFound) { + return createSourceStampResultWithError( + Result.SourceStampInfo.SourceStampVerificationStatus.STAMP_NOT_VERIFIED, + Issue.SOURCE_STAMP_SIGNATURE_BLOCK_WITHOUT_CERT_DIGEST); + } else { + return createSourceStampResultWithError( + Result.SourceStampInfo.SourceStampVerificationStatus.STAMP_MISSING, + Issue.SOURCE_STAMP_CERT_DIGEST_AND_SIG_BLOCK_MISSING); + } + } + + // Verify that the contents of the source stamp certificate digest match the expected + // value, if provided. + byte[] sourceStampCertificateDigest = + LocalFileRecord.getUncompressedData( + apk, + sourceStampCdRecord, + zipSections.getZipCentralDirectoryOffset()); + if (expectedCertDigest != null) { + String actualCertDigest = ApkSigningBlockUtils.toHex(sourceStampCertificateDigest); + if (!expectedCertDigest.equalsIgnoreCase(actualCertDigest)) { + return createSourceStampResultWithError( + Result.SourceStampInfo.SourceStampVerificationStatus + .CERT_DIGEST_MISMATCH, + Issue.SOURCE_STAMP_EXPECTED_DIGEST_MISMATCH, actualCertDigest, + expectedCertDigest); + } + } + + Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeApkContentDigests = + new HashMap<>(); + Map<Integer, String> supportedSchemeNames = getSupportedSchemeNames(mMaxSdkVersion); + Set<Integer> foundApkSigSchemeIds = new HashSet<>(2); + + Result result = new Result(); + ApkSigningBlockUtils.Result v3Result = null; + if (mMaxSdkVersion >= AndroidSdkVersion.P) { + v3Result = getApkContentDigests(apk, zipSections, foundApkSigSchemeIds, + supportedSchemeNames, signatureSchemeApkContentDigests, + VERSION_APK_SIGNATURE_SCHEME_V3, + Math.max(minSdkVersion, AndroidSdkVersion.P)); + if (v3Result != null && v3Result.containsErrors()) { + result.mergeFrom(v3Result); + return mergeSourceStampResult( + Result.SourceStampInfo.SourceStampVerificationStatus.VERIFICATION_ERROR, + result); + } + } + + ApkSigningBlockUtils.Result v2Result = null; + if (mMaxSdkVersion >= AndroidSdkVersion.N && (minSdkVersion < AndroidSdkVersion.P + || foundApkSigSchemeIds.isEmpty())) { + v2Result = getApkContentDigests(apk, zipSections, foundApkSigSchemeIds, + supportedSchemeNames, signatureSchemeApkContentDigests, + VERSION_APK_SIGNATURE_SCHEME_V2, + Math.max(minSdkVersion, AndroidSdkVersion.N)); + if (v2Result != null && v2Result.containsErrors()) { + result.mergeFrom(v2Result); + return mergeSourceStampResult( + Result.SourceStampInfo.SourceStampVerificationStatus.VERIFICATION_ERROR, + result); + } + } + + if (minSdkVersion < AndroidSdkVersion.N || foundApkSigSchemeIds.isEmpty()) { + signatureSchemeApkContentDigests.put(VERSION_JAR_SIGNATURE_SCHEME, + getApkContentDigestFromV1SigningScheme(cdRecords, apk, zipSections)); + } + + ApkSigResult sourceStampResult = + V2SourceStampVerifier.verify( + apk, + zipSections, + sourceStampCertificateDigest, + signatureSchemeApkContentDigests, + minSdkVersion, + mMaxSdkVersion); + result.mergeFrom(sourceStampResult); + // Since the caller is only seeking to verify the source stamp the Result can be marked + // as verified if the source stamp verification was successful. + if (sourceStampResult.verified) { + result.setVerified(); + } else { + // To prevent APK signature verification with a failed / missing source stamp the + // source stamp verification will only log warnings; to allow the caller to capture + // the failure reason treat all warnings as errors. + result.setWarningsAsErrors(true); + } + return result; + } catch (ApkFormatException | IOException | ZipFormatException e) { + return createSourceStampResultWithError( + Result.SourceStampInfo.SourceStampVerificationStatus.VERIFICATION_ERROR, + Issue.MALFORMED_APK, e); + } catch (NoSuchAlgorithmException e) { + return createSourceStampResultWithError( + Result.SourceStampInfo.SourceStampVerificationStatus.VERIFICATION_ERROR, + Issue.UNEXPECTED_EXCEPTION, e); + } catch (SignatureNotFoundException e) { + return createSourceStampResultWithError( + Result.SourceStampInfo.SourceStampVerificationStatus.STAMP_NOT_VERIFIED, + Issue.SOURCE_STAMP_SIG_MISSING); + } + } + + /** + * Creates and returns a {@code Result} that can be returned for source stamp verification + * with the provided source stamp {@code verificationStatus}, and logs an error for the + * specified {@code issue} and {@code params}. + */ + private static Result createSourceStampResultWithError( + Result.SourceStampInfo.SourceStampVerificationStatus verificationStatus, Issue issue, + Object... params) { + Result result = new Result(); + result.addError(issue, params); + return mergeSourceStampResult(verificationStatus, result); + } + + /** + * Creates a new {@link Result.SourceStampInfo} under the provided {@code result} and sets the + * source stamp status to the provided {@code verificationStatus}. + */ + private static Result mergeSourceStampResult( + Result.SourceStampInfo.SourceStampVerificationStatus verificationStatus, + Result result) { + result.mSourceStampInfo = new Result.SourceStampInfo(verificationStatus); + return result; + } + + /** + * Gets content digests, signing lineage and certificates from the given {@code schemeId} block + * alongside encountered errors info and creates a new {@code Result} containing all this + * information. + */ + public static Result getSigningBlockResult( + DataSource apk, ApkUtils.ZipSections zipSections, int sdkVersion, int schemeId) + throws IOException, NoSuchAlgorithmException{ + Map<Integer, Map<ContentDigestAlgorithm, byte[]>> sigSchemeApkContentDigests = + new HashMap<>(); + Map<Integer, String> supportedSchemeNames = getSupportedSchemeNames(sdkVersion); + Set<Integer> foundApkSigSchemeIds = new HashSet<>(2); + + Result result = new Result(); + result.mergeFrom(getApkContentDigests(apk, zipSections, + foundApkSigSchemeIds, supportedSchemeNames, sigSchemeApkContentDigests, + schemeId, sdkVersion, sdkVersion)); + return result; + } + + /** + * Gets the content digest from the {@code result}'s signers. Ignores {@code ContentDigest}s + * for which {@code SignatureAlgorithm} is {@code null}. + */ + public static Map<ContentDigestAlgorithm, byte[]> getContentDigestsFromResult( + Result result, int schemeId) { + Map<ContentDigestAlgorithm, byte[]> apkContentDigests = new HashMap<>(); + if (!(schemeId == VERSION_APK_SIGNATURE_SCHEME_V2 + || schemeId == VERSION_APK_SIGNATURE_SCHEME_V3 + || schemeId == VERSION_APK_SIGNATURE_SCHEME_V31)) { + return apkContentDigests; + } + switch (schemeId) { + case VERSION_APK_SIGNATURE_SCHEME_V2: + for (V2SchemeSignerInfo signerInfo : result.getV2SchemeSigners()) { + getContentDigests(signerInfo.getContentDigests(), apkContentDigests); + } + break; + case VERSION_APK_SIGNATURE_SCHEME_V3: + for (Result.V3SchemeSignerInfo signerInfo : result.getV3SchemeSigners()) { + getContentDigests(signerInfo.getContentDigests(), apkContentDigests); + } + break; + case VERSION_APK_SIGNATURE_SCHEME_V31: + for (Result.V3SchemeSignerInfo signerInfo : result.getV31SchemeSigners()) { + getContentDigests(signerInfo.getContentDigests(), apkContentDigests); + } + break; + } + return apkContentDigests; + } + + private static void getContentDigests( + List<ContentDigest> digests, Map<ContentDigestAlgorithm, byte[]> contentDigestsMap) { + for (ApkSigningBlockUtils.Result.SignerInfo.ContentDigest contentDigest : + digests) { + SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.findById( + contentDigest.getSignatureAlgorithmId()); + if (signatureAlgorithm == null) { + continue; + } + contentDigestsMap.put(signatureAlgorithm.getContentDigestAlgorithm(), + contentDigest.getValue()); + } + } + + /** + * Checks whether a given {@code result} contains errors indicating that a signing certificate + * lineage is incorrect. + */ + public static boolean containsLineageErrors( + Result result) { + if (!result.containsErrors()) { + return false; + } + + return (result.getAllErrors().stream().map(i -> i.getIssue()) + .anyMatch(error -> LINEAGE_RELATED_ISSUES.contains(error))); + } + + + /** + * Gets a lineage from the first signer from a given {@code result}. + * If the {@code result} contains errors related to the lineage incorrectness or there are no + * signers or certificates, it returns {@code null}. + * If the lineage is empty but there is a signer, it returns a 1-element lineage containing + * the signing key. + */ + public static SigningCertificateLineage getLineageFromResult( + Result result, int sdkVersion, int schemeId) + throws CertificateEncodingException, InvalidKeyException, NoSuchAlgorithmException, + SignatureException { + if (!(schemeId == VERSION_APK_SIGNATURE_SCHEME_V3 + || schemeId == VERSION_APK_SIGNATURE_SCHEME_V31) + || containsLineageErrors(result)) { + return null; + } + List<V3SchemeSignerInfo> signersInfo = + schemeId == VERSION_APK_SIGNATURE_SCHEME_V3 ? + result.getV3SchemeSigners() : result.getV31SchemeSigners(); + if (signersInfo.isEmpty()) { + return null; + } + V3SchemeSignerInfo firstSignerInfo = signersInfo.get(0); + SigningCertificateLineage lineage = firstSignerInfo.mSigningCertificateLineage; + if (lineage == null && firstSignerInfo.getCertificate() != null) { + try { + lineage = new SigningCertificateLineage.Builder( + new SignerConfig.Builder( + /* privateKey= */ null, firstSignerInfo.getCertificate()) + .build()).build(); + } catch (Exception e) { + return null; + } + } + return lineage; + } + + /** + * Obtains the APK content digest(s) and adds them to the provided {@code + * sigSchemeApkContentDigests}, returning an {@code ApkSigningBlockUtils.Result} that can be + * merged with a {@code Result} to notify the client of any errors. + * + * <p>Note, this method currently only supports signature scheme V2 and V3; to obtain the + * content digests for V1 signatures use {@link + * #getApkContentDigestFromV1SigningScheme(List, DataSource, ApkUtils.ZipSections)}. If a + * signature scheme version other than V2 or V3 is provided a {@code null} value will be + * returned. + */ + private ApkSigningBlockUtils.Result getApkContentDigests(DataSource apk, + ApkUtils.ZipSections zipSections, Set<Integer> foundApkSigSchemeIds, + Map<Integer, String> supportedSchemeNames, + Map<Integer, Map<ContentDigestAlgorithm, byte[]>> sigSchemeApkContentDigests, + int apkSigSchemeVersion, int minSdkVersion) + throws IOException, NoSuchAlgorithmException { + return getApkContentDigests(apk, zipSections, foundApkSigSchemeIds, supportedSchemeNames, + sigSchemeApkContentDigests, apkSigSchemeVersion, minSdkVersion, mMaxSdkVersion); + } + + + /** + * Obtains the APK content digest(s) and adds them to the provided {@code + * sigSchemeApkContentDigests}, returning an {@code ApkSigningBlockUtils.Result} that can be + * merged with a {@code Result} to notify the client of any errors. + * + * <p>Note, this method currently only supports signature scheme V2 and V3; to obtain the + * content digests for V1 signatures use {@link + * #getApkContentDigestFromV1SigningScheme(List, DataSource, ApkUtils.ZipSections)}. If a + * signature scheme version other than V2 or V3 is provided a {@code null} value will be + * returned. + */ + private static ApkSigningBlockUtils.Result getApkContentDigests(DataSource apk, + ApkUtils.ZipSections zipSections, Set<Integer> foundApkSigSchemeIds, + Map<Integer, String> supportedSchemeNames, + Map<Integer, Map<ContentDigestAlgorithm, byte[]>> sigSchemeApkContentDigests, + int apkSigSchemeVersion, int minSdkVersion, int maxSdkVersion) + throws IOException, NoSuchAlgorithmException { + if (!(apkSigSchemeVersion == VERSION_APK_SIGNATURE_SCHEME_V2 + || apkSigSchemeVersion == VERSION_APK_SIGNATURE_SCHEME_V3 + || apkSigSchemeVersion == VERSION_APK_SIGNATURE_SCHEME_V31)) { + return null; + } + ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result(apkSigSchemeVersion); + SignatureInfo signatureInfo; + try { + int sigSchemeBlockId; + switch (apkSigSchemeVersion) { + case VERSION_APK_SIGNATURE_SCHEME_V31: + sigSchemeBlockId = V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID; + break; + case VERSION_APK_SIGNATURE_SCHEME_V3: + sigSchemeBlockId = V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID; + break; + default: + sigSchemeBlockId = + V2SchemeConstants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID; + } + signatureInfo = ApkSigningBlockUtils.findSignature(apk, zipSections, + sigSchemeBlockId, result); + } catch (ApkSigningBlockUtils.SignatureNotFoundException e) { + return null; + } + foundApkSigSchemeIds.add(apkSigSchemeVersion); + + Set<ContentDigestAlgorithm> contentDigestsToVerify = new HashSet<>(1); + if (apkSigSchemeVersion == VERSION_APK_SIGNATURE_SCHEME_V2) { + V2SchemeVerifier.parseSigners(signatureInfo.signatureBlock, + contentDigestsToVerify, supportedSchemeNames, + foundApkSigSchemeIds, minSdkVersion, maxSdkVersion, result); + } else { + V3SchemeVerifier.parseSigners(signatureInfo.signatureBlock, + contentDigestsToVerify, result); + } + Map<ContentDigestAlgorithm, byte[]> apkContentDigests = new EnumMap<>( + ContentDigestAlgorithm.class); + for (ApkSigningBlockUtils.Result.SignerInfo signerInfo : result.signers) { + for (ApkSigningBlockUtils.Result.SignerInfo.ContentDigest contentDigest : + signerInfo.contentDigests) { + SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.findById( + contentDigest.getSignatureAlgorithmId()); + if (signatureAlgorithm == null) { + continue; + } + apkContentDigests.put(signatureAlgorithm.getContentDigestAlgorithm(), + contentDigest.getValue()); + } + } + sigSchemeApkContentDigests.put(apkSigSchemeVersion, apkContentDigests); + return result; + } + + private static void checkV4Signer(List<Result.V3SchemeSignerInfo> v3Signers, + List<X509Certificate> v4Certs, byte[] digestFromV4, Result result) { + if (v3Signers.size() != 1) { + result.addError(Issue.V4_SIG_MULTIPLE_SIGNERS); + } + + // Compare certificates. + checkV4Certificate(v4Certs, v3Signers.get(0).mCerts, result); + + // Compare digests. + final byte[] digestFromV3 = pickBestDigestForV4(v3Signers.get(0).getContentDigests()); + if (!Arrays.equals(digestFromV4, digestFromV3)) { + result.addError(Issue.V4_SIG_V2_V3_DIGESTS_MISMATCH, 3, toHex(digestFromV3), + toHex(digestFromV4)); + } + } + + private static void checkV4Certificate(List<X509Certificate> v4Certs, + List<X509Certificate> v2v3Certs, Result result) { + try { + byte[] v4Cert = v4Certs.get(0).getEncoded(); + byte[] cert = v2v3Certs.get(0).getEncoded(); + if (!Arrays.equals(cert, v4Cert)) { + result.addError(Issue.V4_SIG_V2_V3_SIGNERS_MISMATCH); + } + } catch (CertificateEncodingException e) { + throw new RuntimeException("Failed to encode APK signer cert", e); + } + } + + private static byte[] pickBestDigestForV4( + List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> contentDigests) { + Map<ContentDigestAlgorithm, byte[]> apkContentDigests = new HashMap<>(); + collectApkContentDigests(contentDigests, apkContentDigests); + return ApkSigningBlockUtils.pickBestDigestForV4(apkContentDigests); + } + + private static Map<ContentDigestAlgorithm, byte[]> getApkContentDigestsFromSigningSchemeResult( + ApkSigningBlockUtils.Result apkSigningSchemeResult) { + Map<ContentDigestAlgorithm, byte[]> apkContentDigests = new HashMap<>(); + for (ApkSigningBlockUtils.Result.SignerInfo signerInfo : apkSigningSchemeResult.signers) { + collectApkContentDigests(signerInfo.contentDigests, apkContentDigests); + } + return apkContentDigests; + } + + private static Map<ContentDigestAlgorithm, byte[]> getApkContentDigestFromV1SigningScheme( + List<CentralDirectoryRecord> cdRecords, + DataSource apk, + ApkUtils.ZipSections zipSections) + throws IOException, ApkFormatException { + CentralDirectoryRecord manifestCdRecord = null; + Map<ContentDigestAlgorithm, byte[]> v1ContentDigest = new EnumMap<>( + ContentDigestAlgorithm.class); + for (CentralDirectoryRecord cdRecord : cdRecords) { + if (MANIFEST_ENTRY_NAME.equals(cdRecord.getName())) { + manifestCdRecord = cdRecord; + break; + } + } + if (manifestCdRecord == null) { + // No JAR signing manifest file found. For SourceStamp verification, returning an empty + // digest is enough since this would affect the final digest signed by the stamp, and + // thus an empty digest will invalidate that signature. + return v1ContentDigest; + } + try { + byte[] manifestBytes = + LocalFileRecord.getUncompressedData( + apk, manifestCdRecord, zipSections.getZipCentralDirectoryOffset()); + v1ContentDigest.put( + ContentDigestAlgorithm.SHA256, computeSha256DigestBytes(manifestBytes)); + return v1ContentDigest; + } catch (ZipFormatException e) { + throw new ApkFormatException("Failed to read APK", e); + } + } + + private static void collectApkContentDigests( + List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> contentDigests, + Map<ContentDigestAlgorithm, byte[]> apkContentDigests) { + for (ApkSigningBlockUtils.Result.SignerInfo.ContentDigest contentDigest : contentDigests) { + SignatureAlgorithm signatureAlgorithm = + SignatureAlgorithm.findById(contentDigest.getSignatureAlgorithmId()); + if (signatureAlgorithm == null) { + continue; + } + ContentDigestAlgorithm contentDigestAlgorithm = + signatureAlgorithm.getContentDigestAlgorithm(); + apkContentDigests.put(contentDigestAlgorithm, contentDigest.getValue()); + } + + } + + private static ByteBuffer getAndroidManifestFromApk( + DataSource apk, ApkUtils.ZipSections zipSections) + throws IOException, ApkFormatException { + List<CentralDirectoryRecord> cdRecords = + V1SchemeVerifier.parseZipCentralDirectory(apk, zipSections); + try { + return ApkSigner.getAndroidManifestFromApk( + cdRecords, + apk.slice(0, zipSections.getZipCentralDirectoryOffset())); + } catch (ZipFormatException e) { + throw new ApkFormatException("Failed to read AndroidManifest.xml", e); + } + } + + private static int getMinimumSignatureSchemeVersionForTargetSdk(int targetSdkVersion) { + if (targetSdkVersion >= AndroidSdkVersion.R) { + return VERSION_APK_SIGNATURE_SCHEME_V2; + } + return VERSION_JAR_SIGNATURE_SCHEME; + } + + /** + * Result of verifying an APKs signatures. The APK can be considered verified iff + * {@link #isVerified()} returns {@code true}. + */ + public static class Result { + private final List<IssueWithParams> mErrors = new ArrayList<>(); + private final List<IssueWithParams> mWarnings = new ArrayList<>(); + private final List<X509Certificate> mSignerCerts = new ArrayList<>(); + private final List<V1SchemeSignerInfo> mV1SchemeSigners = new ArrayList<>(); + private final List<V1SchemeSignerInfo> mV1SchemeIgnoredSigners = new ArrayList<>(); + private final List<V2SchemeSignerInfo> mV2SchemeSigners = new ArrayList<>(); + private final List<V3SchemeSignerInfo> mV3SchemeSigners = new ArrayList<>(); + private final List<V3SchemeSignerInfo> mV31SchemeSigners = new ArrayList<>(); + private final List<V4SchemeSignerInfo> mV4SchemeSigners = new ArrayList<>(); + private SourceStampInfo mSourceStampInfo; + + private boolean mVerified; + private boolean mVerifiedUsingV1Scheme; + private boolean mVerifiedUsingV2Scheme; + private boolean mVerifiedUsingV3Scheme; + private boolean mVerifiedUsingV31Scheme; + private boolean mVerifiedUsingV4Scheme; + private boolean mSourceStampVerified; + private boolean mWarningsAsErrors; + private SigningCertificateLineage mSigningCertificateLineage; + + /** + * Returns {@code true} if the APK's signatures verified. + */ + public boolean isVerified() { + return mVerified; + } + + private void setVerified() { + mVerified = true; + } + + /** + * Returns {@code true} if the APK's JAR signatures verified. + */ + public boolean isVerifiedUsingV1Scheme() { + return mVerifiedUsingV1Scheme; + } + + /** + * Returns {@code true} if the APK's APK Signature Scheme v2 signatures verified. + */ + public boolean isVerifiedUsingV2Scheme() { + return mVerifiedUsingV2Scheme; + } + + /** + * Returns {@code true} if the APK's APK Signature Scheme v3 signature verified. + */ + public boolean isVerifiedUsingV3Scheme() { + return mVerifiedUsingV3Scheme; + } + + /** + * Returns {@code true} if the APK's APK Signature Scheme v3.1 signature verified. + */ + public boolean isVerifiedUsingV31Scheme() { + return mVerifiedUsingV31Scheme; + } + + /** + * Returns {@code true} if the APK's APK Signature Scheme v4 signature verified. + */ + public boolean isVerifiedUsingV4Scheme() { + return mVerifiedUsingV4Scheme; + } + + /** + * Returns {@code true} if the APK's SourceStamp signature verified. + */ + public boolean isSourceStampVerified() { + return mSourceStampVerified; + } + + /** + * Returns the verified signers' certificates, one per signer. + */ + public List<X509Certificate> getSignerCertificates() { + return mSignerCerts; + } + + private void addSignerCertificate(X509Certificate cert) { + mSignerCerts.add(cert); + } + + /** + * Returns information about JAR signers associated with the APK's signature. These are the + * signers used by Android. + * + * @see #getV1SchemeIgnoredSigners() + */ + public List<V1SchemeSignerInfo> getV1SchemeSigners() { + return mV1SchemeSigners; + } + + /** + * Returns information about JAR signers ignored by the APK's signature verification + * process. These signers are ignored by Android. However, each signer's errors or warnings + * will contain information about why they are ignored. + * + * @see #getV1SchemeSigners() + */ + public List<V1SchemeSignerInfo> getV1SchemeIgnoredSigners() { + return mV1SchemeIgnoredSigners; + } + + /** + * Returns information about APK Signature Scheme v2 signers associated with the APK's + * signature. + */ + public List<V2SchemeSignerInfo> getV2SchemeSigners() { + return mV2SchemeSigners; + } + + /** + * Returns information about APK Signature Scheme v3 signers associated with the APK's + * signature. + * + * <note> Multiple signers represent different targeted platform versions, not + * a signing identity of multiple signers. APK Signature Scheme v3 only supports single + * signer identities.</note> + */ + public List<V3SchemeSignerInfo> getV3SchemeSigners() { + return mV3SchemeSigners; + } + + /** + * Returns information about APK Signature Scheme v3.1 signers associated with the APK's + * signature. + * + * <note> Multiple signers represent different targeted platform versions, not + * a signing identity of multiple signers. APK Signature Scheme v3.1 only supports single + * signer identities.</note> + */ + public List<V3SchemeSignerInfo> getV31SchemeSigners() { + return mV31SchemeSigners; + } + + /** + * Returns information about APK Signature Scheme v4 signers associated with the APK's + * signature. + */ + public List<V4SchemeSignerInfo> getV4SchemeSigners() { + return mV4SchemeSigners; + } + + /** + * Returns information about SourceStamp associated with the APK's signature. + */ + public SourceStampInfo getSourceStampInfo() { + return mSourceStampInfo; + } + + /** + * Returns the combined SigningCertificateLineage associated with this APK's APK Signature + * Scheme v3 signing block. + */ + public SigningCertificateLineage getSigningCertificateLineage() { + return mSigningCertificateLineage; + } + + void addError(Issue msg, Object... parameters) { + mErrors.add(new IssueWithParams(msg, parameters)); + } + + void addWarning(Issue msg, Object... parameters) { + mWarnings.add(new IssueWithParams(msg, parameters)); + } + + /** + * Sets whether warnings should be treated as errors. + */ + void setWarningsAsErrors(boolean value) { + mWarningsAsErrors = value; + } + + /** + * Returns errors encountered while verifying the APK's signatures. + */ + public List<IssueWithParams> getErrors() { + if (!mWarningsAsErrors) { + return mErrors; + } else { + List<IssueWithParams> allErrors = new ArrayList<>(); + allErrors.addAll(mErrors); + allErrors.addAll(mWarnings); + return allErrors; + } + } + + /** + * Returns warnings encountered while verifying the APK's signatures. + */ + public List<IssueWithParams> getWarnings() { + return mWarnings; + } + + private void mergeFrom(V1SchemeVerifier.Result source) { + mVerifiedUsingV1Scheme = source.verified; + mErrors.addAll(source.getErrors()); + mWarnings.addAll(source.getWarnings()); + for (V1SchemeVerifier.Result.SignerInfo signer : source.signers) { + mV1SchemeSigners.add(new V1SchemeSignerInfo(signer)); + } + for (V1SchemeVerifier.Result.SignerInfo signer : source.ignoredSigners) { + mV1SchemeIgnoredSigners.add(new V1SchemeSignerInfo(signer)); + } + } + + private void mergeFrom(ApkSigResult source) { + switch (source.signatureSchemeVersion) { + case VERSION_SOURCE_STAMP: + mSourceStampVerified = source.verified; + if (!source.mSigners.isEmpty()) { + mSourceStampInfo = new SourceStampInfo(source.mSigners.get(0)); + } + break; + default: + throw new IllegalArgumentException( + "Unknown ApkSigResult Signing Block Scheme Id " + + source.signatureSchemeVersion); + } + } + + private void mergeFrom(ApkSigningBlockUtils.Result source) { + if (source == null) { + return; + } + if (source.containsErrors()) { + mErrors.addAll(source.getErrors()); + } + if (source.containsWarnings()) { + mWarnings.addAll(source.getWarnings()); + } + switch (source.signatureSchemeVersion) { + case VERSION_APK_SIGNATURE_SCHEME_V2: + mVerifiedUsingV2Scheme = source.verified; + for (ApkSigningBlockUtils.Result.SignerInfo signer : source.signers) { + mV2SchemeSigners.add(new V2SchemeSignerInfo(signer)); + } + break; + case VERSION_APK_SIGNATURE_SCHEME_V3: + mVerifiedUsingV3Scheme = source.verified; + for (ApkSigningBlockUtils.Result.SignerInfo signer : source.signers) { + mV3SchemeSigners.add(new V3SchemeSignerInfo(signer)); + } + // Do not overwrite a previously set lineage from a v3.1 signing block. + if (mSigningCertificateLineage == null) { + mSigningCertificateLineage = source.signingCertificateLineage; + } + break; + case VERSION_APK_SIGNATURE_SCHEME_V31: + mVerifiedUsingV31Scheme = source.verified; + for (ApkSigningBlockUtils.Result.SignerInfo signer : source.signers) { + mV31SchemeSigners.add(new V3SchemeSignerInfo(signer)); + } + mSigningCertificateLineage = source.signingCertificateLineage; + break; + case VERSION_APK_SIGNATURE_SCHEME_V4: + mVerifiedUsingV4Scheme = source.verified; + for (ApkSigningBlockUtils.Result.SignerInfo signer : source.signers) { + mV4SchemeSigners.add(new V4SchemeSignerInfo(signer)); + } + break; + case VERSION_SOURCE_STAMP: + mSourceStampVerified = source.verified; + if (!source.signers.isEmpty()) { + mSourceStampInfo = new SourceStampInfo(source.signers.get(0)); + } + break; + default: + throw new IllegalArgumentException("Unknown Signing Block Scheme Id"); + } + } + + /** + * Returns {@code true} if an error was encountered while verifying the APK. Any error + * prevents the APK from being considered verified. + */ + public boolean containsErrors() { + if (!mErrors.isEmpty()) { + return true; + } + if (mWarningsAsErrors && !mWarnings.isEmpty()) { + return true; + } + if (!mV1SchemeSigners.isEmpty()) { + for (V1SchemeSignerInfo signer : mV1SchemeSigners) { + if (signer.containsErrors()) { + return true; + } + if (mWarningsAsErrors && !signer.getWarnings().isEmpty()) { + return true; + } + } + } + if (!mV2SchemeSigners.isEmpty()) { + for (V2SchemeSignerInfo signer : mV2SchemeSigners) { + if (signer.containsErrors()) { + return true; + } + if (mWarningsAsErrors && !signer.getWarnings().isEmpty()) { + return true; + } + } + } + if (!mV3SchemeSigners.isEmpty()) { + for (V3SchemeSignerInfo signer : mV3SchemeSigners) { + if (signer.containsErrors()) { + return true; + } + if (mWarningsAsErrors && !signer.getWarnings().isEmpty()) { + return true; + } + } + } + if (!mV31SchemeSigners.isEmpty()) { + for (V3SchemeSignerInfo signer : mV31SchemeSigners) { + if (signer.containsErrors()) { + return true; + } + if (mWarningsAsErrors && !signer.getWarnings().isEmpty()) { + return true; + } + } + } + if (mSourceStampInfo != null) { + if (mSourceStampInfo.containsErrors()) { + return true; + } + if (mWarningsAsErrors && !mSourceStampInfo.getWarnings().isEmpty()) { + return true; + } + } + + return false; + } + + /** + * Returns all errors for this result, including any errors from signature scheme signers + * and the source stamp. + */ + public List<IssueWithParams> getAllErrors() { + List<IssueWithParams> errors = new ArrayList<>(); + errors.addAll(mErrors); + if (mWarningsAsErrors) { + errors.addAll(mWarnings); + } + if (!mV1SchemeSigners.isEmpty()) { + for (V1SchemeSignerInfo signer : mV1SchemeSigners) { + errors.addAll(signer.mErrors); + if (mWarningsAsErrors) { + errors.addAll(signer.getWarnings()); + } + } + } + if (!mV2SchemeSigners.isEmpty()) { + for (V2SchemeSignerInfo signer : mV2SchemeSigners) { + errors.addAll(signer.mErrors); + if (mWarningsAsErrors) { + errors.addAll(signer.getWarnings()); + } + } + } + if (!mV3SchemeSigners.isEmpty()) { + for (V3SchemeSignerInfo signer : mV3SchemeSigners) { + errors.addAll(signer.mErrors); + if (mWarningsAsErrors) { + errors.addAll(signer.getWarnings()); + } + } + } + if (!mV31SchemeSigners.isEmpty()) { + for (V3SchemeSignerInfo signer : mV31SchemeSigners) { + errors.addAll(signer.mErrors); + if (mWarningsAsErrors) { + errors.addAll(signer.getWarnings()); + } + } + } + if (mSourceStampInfo != null) { + errors.addAll(mSourceStampInfo.getErrors()); + if (mWarningsAsErrors) { + errors.addAll(mSourceStampInfo.getWarnings()); + } + } + return errors; + } + + /** + * Information about a JAR signer associated with the APK's signature. + */ + public static class V1SchemeSignerInfo { + private final String mName; + private final List<X509Certificate> mCertChain; + private final String mSignatureBlockFileName; + private final String mSignatureFileName; + + private final List<IssueWithParams> mErrors; + private final List<IssueWithParams> mWarnings; + + private V1SchemeSignerInfo(V1SchemeVerifier.Result.SignerInfo result) { + mName = result.name; + mCertChain = result.certChain; + mSignatureBlockFileName = result.signatureBlockFileName; + mSignatureFileName = result.signatureFileName; + mErrors = result.getErrors(); + mWarnings = result.getWarnings(); + } + + /** + * Returns a user-friendly name of the signer. + */ + public String getName() { + return mName; + } + + /** + * Returns the name of the JAR entry containing this signer's JAR signature block file. + */ + public String getSignatureBlockFileName() { + return mSignatureBlockFileName; + } + + /** + * Returns the name of the JAR entry containing this signer's JAR signature file. + */ + public String getSignatureFileName() { + return mSignatureFileName; + } + + /** + * Returns this signer's signing certificate or {@code null} if not available. The + * certificate is guaranteed to be available if no errors were encountered during + * verification (see {@link #containsErrors()}. + * + * <p>This certificate contains the signer's public key. + */ + public X509Certificate getCertificate() { + return mCertChain.isEmpty() ? null : mCertChain.get(0); + } + + /** + * Returns the certificate chain for the signer's public key. The certificate containing + * the public key is first, followed by the certificate (if any) which issued the + * signing certificate, and so forth. An empty list may be returned if an error was + * encountered during verification (see {@link #containsErrors()}). + */ + public List<X509Certificate> getCertificateChain() { + return mCertChain; + } + + /** + * Returns {@code true} if an error was encountered while verifying this signer's JAR + * signature. Any error prevents the signer's signature from being considered verified. + */ + public boolean containsErrors() { + return !mErrors.isEmpty(); + } + + /** + * Returns errors encountered while verifying this signer's JAR signature. Any error + * prevents the signer's signature from being considered verified. + */ + public List<IssueWithParams> getErrors() { + return mErrors; + } + + /** + * Returns warnings encountered while verifying this signer's JAR signature. Warnings + * do not prevent the signer's signature from being considered verified. + */ + public List<IssueWithParams> getWarnings() { + return mWarnings; + } + + private void addError(Issue msg, Object... parameters) { + mErrors.add(new IssueWithParams(msg, parameters)); + } + } + + /** + * Information about an APK Signature Scheme v2 signer associated with the APK's signature. + */ + public static class V2SchemeSignerInfo { + private final int mIndex; + private final List<X509Certificate> mCerts; + + private final List<IssueWithParams> mErrors; + private final List<IssueWithParams> mWarnings; + private final List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> + mContentDigests; + + private V2SchemeSignerInfo(ApkSigningBlockUtils.Result.SignerInfo result) { + mIndex = result.index; + mCerts = result.certs; + mErrors = result.getErrors(); + mWarnings = result.getWarnings(); + mContentDigests = result.contentDigests; + } + + /** + * Returns this signer's {@code 0}-based index in the list of signers contained in the + * APK's APK Signature Scheme v2 signature. + */ + public int getIndex() { + return mIndex; + } + + /** + * Returns this signer's signing certificate or {@code null} if not available. The + * certificate is guaranteed to be available if no errors were encountered during + * verification (see {@link #containsErrors()}. + * + * <p>This certificate contains the signer's public key. + */ + public X509Certificate getCertificate() { + return mCerts.isEmpty() ? null : mCerts.get(0); + } + + /** + * Returns this signer's certificates. The first certificate is for the signer's public + * key. An empty list may be returned if an error was encountered during verification + * (see {@link #containsErrors()}). + */ + public List<X509Certificate> getCertificates() { + return mCerts; + } + + private void addError(Issue msg, Object... parameters) { + mErrors.add(new IssueWithParams(msg, parameters)); + } + + public boolean containsErrors() { + return !mErrors.isEmpty(); + } + + public List<IssueWithParams> getErrors() { + return mErrors; + } + + public List<IssueWithParams> getWarnings() { + return mWarnings; + } + + public List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> getContentDigests() { + return mContentDigests; + } + } + + /** + * Information about an APK Signature Scheme v3 signer associated with the APK's signature. + */ + public static class V3SchemeSignerInfo { + private final int mIndex; + private final List<X509Certificate> mCerts; + + private final List<IssueWithParams> mErrors; + private final List<IssueWithParams> mWarnings; + private final List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> + mContentDigests; + private final int mMinSdkVersion; + private final int mMaxSdkVersion; + private final boolean mRotationTargetsDevRelease; + private final SigningCertificateLineage mSigningCertificateLineage; + + private V3SchemeSignerInfo(ApkSigningBlockUtils.Result.SignerInfo result) { + mIndex = result.index; + mCerts = result.certs; + mErrors = result.getErrors(); + mWarnings = result.getWarnings(); + mContentDigests = result.contentDigests; + mMinSdkVersion = result.minSdkVersion; + mMaxSdkVersion = result.maxSdkVersion; + mSigningCertificateLineage = result.signingCertificateLineage; + mRotationTargetsDevRelease = result.additionalAttributes.stream().mapToInt( + attribute -> attribute.getId()).anyMatch( + attrId -> attrId == V3SchemeConstants.ROTATION_ON_DEV_RELEASE_ATTR_ID); + } + + /** + * Returns this signer's {@code 0}-based index in the list of signers contained in the + * APK's APK Signature Scheme v3 signature. + */ + public int getIndex() { + return mIndex; + } + + /** + * Returns this signer's signing certificate or {@code null} if not available. The + * certificate is guaranteed to be available if no errors were encountered during + * verification (see {@link #containsErrors()}. + * + * <p>This certificate contains the signer's public key. + */ + public X509Certificate getCertificate() { + return mCerts.isEmpty() ? null : mCerts.get(0); + } + + /** + * Returns this signer's certificates. The first certificate is for the signer's public + * key. An empty list may be returned if an error was encountered during verification + * (see {@link #containsErrors()}). + */ + public List<X509Certificate> getCertificates() { + return mCerts; + } + + public boolean containsErrors() { + return !mErrors.isEmpty(); + } + + public List<IssueWithParams> getErrors() { + return mErrors; + } + + public List<IssueWithParams> getWarnings() { + return mWarnings; + } + + public List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> getContentDigests() { + return mContentDigests; + } + + /** + * Returns the minimum SDK version on which this signer should be verified. + */ + public int getMinSdkVersion() { + return mMinSdkVersion; + } + + /** + * Returns the maximum SDK version on which this signer should be verified. + */ + public int getMaxSdkVersion() { + return mMaxSdkVersion; + } + + /** + * Returns whether rotation is targeting a development release. + * + * <p>A development release uses the SDK version of the previously released platform + * until the SDK of the development release is finalized. To allow rotation to target + * a development release after T, this attribute must be set to ensure rotation is + * used on the development release but ignored on the released platform with the same + * API level. + */ + public boolean getRotationTargetsDevRelease() { + return mRotationTargetsDevRelease; + } + + /** + * Returns the {@link SigningCertificateLineage} for this signer; when an APK has + * SDK targeted signing configs, the lineage of each signer could potentially contain + * a subset of the full signing lineage and / or different capabilities for each signer + * in the lineage. + */ + public SigningCertificateLineage getSigningCertificateLineage() { + return mSigningCertificateLineage; + } + } + + /** + * Information about an APK Signature Scheme V4 signer associated with the APK's + * signature. + */ + public static class V4SchemeSignerInfo { + private final int mIndex; + private final List<X509Certificate> mCerts; + + private final List<IssueWithParams> mErrors; + private final List<IssueWithParams> mWarnings; + private final List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> + mContentDigests; + + private V4SchemeSignerInfo(ApkSigningBlockUtils.Result.SignerInfo result) { + mIndex = result.index; + mCerts = result.certs; + mErrors = result.getErrors(); + mWarnings = result.getWarnings(); + mContentDigests = result.contentDigests; + } + + /** + * Returns this signer's {@code 0}-based index in the list of signers contained in the + * APK's APK Signature Scheme v3 signature. + */ + public int getIndex() { + return mIndex; + } + + /** + * Returns this signer's signing certificate or {@code null} if not available. The + * certificate is guaranteed to be available if no errors were encountered during + * verification (see {@link #containsErrors()}. + * + * <p>This certificate contains the signer's public key. + */ + public X509Certificate getCertificate() { + return mCerts.isEmpty() ? null : mCerts.get(0); + } + + /** + * Returns this signer's certificates. The first certificate is for the signer's public + * key. An empty list may be returned if an error was encountered during verification + * (see {@link #containsErrors()}). + */ + public List<X509Certificate> getCertificates() { + return mCerts; + } + + public boolean containsErrors() { + return !mErrors.isEmpty(); + } + + public List<IssueWithParams> getErrors() { + return mErrors; + } + + public List<IssueWithParams> getWarnings() { + return mWarnings; + } + + public List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> getContentDigests() { + return mContentDigests; + } + } + + /** + * Information about SourceStamp associated with the APK's signature. + */ + public static class SourceStampInfo { + public enum SourceStampVerificationStatus { + /** The stamp is present and was successfully verified. */ + STAMP_VERIFIED, + /** The stamp is present but failed verification. */ + STAMP_VERIFICATION_FAILED, + /** The expected cert digest did not match the digest in the APK. */ + CERT_DIGEST_MISMATCH, + /** The stamp is not present at all. */ + STAMP_MISSING, + /** The stamp is at least partially present, but was not able to be verified. */ + STAMP_NOT_VERIFIED, + /** The stamp was not able to be verified due to an unexpected error. */ + VERIFICATION_ERROR + } + + private final List<X509Certificate> mCertificates; + private final List<X509Certificate> mCertificateLineage; + + private final List<IssueWithParams> mErrors; + private final List<IssueWithParams> mWarnings; + private final List<IssueWithParams> mInfoMessages; + + private final SourceStampVerificationStatus mSourceStampVerificationStatus; + + private final long mTimestamp; + + private SourceStampInfo(ApkSignerInfo result) { + mCertificates = result.certs; + mCertificateLineage = result.certificateLineage; + mErrors = ApkVerificationIssueAdapter.getIssuesFromVerificationIssues( + result.getErrors()); + mWarnings = ApkVerificationIssueAdapter.getIssuesFromVerificationIssues( + result.getWarnings()); + mInfoMessages = ApkVerificationIssueAdapter.getIssuesFromVerificationIssues( + result.getInfoMessages()); + if (mErrors.isEmpty() && mWarnings.isEmpty()) { + mSourceStampVerificationStatus = SourceStampVerificationStatus.STAMP_VERIFIED; + } else { + mSourceStampVerificationStatus = + SourceStampVerificationStatus.STAMP_VERIFICATION_FAILED; + } + mTimestamp = result.timestamp; + } + + SourceStampInfo(SourceStampVerificationStatus sourceStampVerificationStatus) { + mCertificates = Collections.emptyList(); + mCertificateLineage = Collections.emptyList(); + mErrors = Collections.emptyList(); + mWarnings = Collections.emptyList(); + mInfoMessages = Collections.emptyList(); + mSourceStampVerificationStatus = sourceStampVerificationStatus; + mTimestamp = 0; + } + + /** + * Returns the SourceStamp's signing certificate or {@code null} if not available. The + * certificate is guaranteed to be available if no errors were encountered during + * verification (see {@link #containsErrors()}. + * + * <p>This certificate contains the SourceStamp's public key. + */ + public X509Certificate getCertificate() { + return mCertificates.isEmpty() ? null : mCertificates.get(0); + } + + /** + * Returns a list containing all of the certificates in the stamp certificate lineage. + */ + public List<X509Certificate> getCertificatesInLineage() { + return mCertificateLineage; + } + + public boolean containsErrors() { + return !mErrors.isEmpty(); + } + + /** + * Returns {@code true} if any info messages were encountered during verification of + * this source stamp. + */ + public boolean containsInfoMessages() { + return !mInfoMessages.isEmpty(); + } + + public List<IssueWithParams> getErrors() { + return mErrors; + } + + public List<IssueWithParams> getWarnings() { + return mWarnings; + } + + /** + * Returns a {@code List} of {@link IssueWithParams} representing info messages + * that were encountered during verification of the source stamp. + */ + public List<IssueWithParams> getInfoMessages() { + return mInfoMessages; + } + + /** + * Returns the reason for any source stamp verification failures, or {@code + * STAMP_VERIFIED} if the source stamp was successfully verified. + */ + public SourceStampVerificationStatus getSourceStampVerificationStatus() { + return mSourceStampVerificationStatus; + } + + /** + * Returns the epoch timestamp in seconds representing the time this source stamp block + * was signed, or 0 if the timestamp is not available. + */ + public long getTimestampEpochSeconds() { + return mTimestamp; + } + } + } + + /** + * Error or warning encountered while verifying an APK's signatures. + */ + public enum Issue { + + /** + * APK is not JAR-signed. + */ + JAR_SIG_NO_SIGNATURES("No JAR signatures"), + + /** + * APK signature scheme v1 has exceeded the maximum number of jar signers. + * <ul> + * <li>Parameter 1: maximum allowed signers ({@code Integer})</li> + * <li>Parameter 2: total number of signers ({@code Integer})</li> + * </ul> + */ + JAR_SIG_MAX_SIGNATURES_EXCEEDED( + "APK Signature Scheme v1 only supports a maximum of %1$d signers, found %2$d"), + + /** + * APK does not contain any entries covered by JAR signatures. + */ + JAR_SIG_NO_SIGNED_ZIP_ENTRIES("No JAR entries covered by JAR signatures"), + + /** + * APK contains multiple entries with the same name. + * + * <ul> + * <li>Parameter 1: name ({@code String})</li> + * </ul> + */ + JAR_SIG_DUPLICATE_ZIP_ENTRY("Duplicate entry: %1$s"), + + /** + * JAR manifest contains a section with a duplicate name. + * + * <ul> + * <li>Parameter 1: section name ({@code String})</li> + * </ul> + */ + JAR_SIG_DUPLICATE_MANIFEST_SECTION("Duplicate section in META-INF/MANIFEST.MF: %1$s"), + + /** + * JAR manifest contains a section without a name. + * + * <ul> + * <li>Parameter 1: section index (1-based) ({@code Integer})</li> + * </ul> + */ + JAR_SIG_UNNNAMED_MANIFEST_SECTION( + "Malformed META-INF/MANIFEST.MF: invidual section #%1$d does not have a name"), + + /** + * JAR signature file contains a section without a name. + * + * <ul> + * <li>Parameter 1: signature file name ({@code String})</li> + * <li>Parameter 2: section index (1-based) ({@code Integer})</li> + * </ul> + */ + JAR_SIG_UNNNAMED_SIG_FILE_SECTION( + "Malformed %1$s: invidual section #%2$d does not have a name"), + + /** APK is missing the JAR manifest entry (META-INF/MANIFEST.MF). */ + JAR_SIG_NO_MANIFEST("Missing META-INF/MANIFEST.MF"), + + /** + * JAR manifest references an entry which is not there in the APK. + * + * <ul> + * <li>Parameter 1: entry name ({@code String})</li> + * </ul> + */ + JAR_SIG_MISSING_ZIP_ENTRY_REFERENCED_IN_MANIFEST( + "%1$s entry referenced by META-INF/MANIFEST.MF not found in the APK"), + + /** + * JAR manifest does not list a digest for the specified entry. + * + * <ul> + * <li>Parameter 1: entry name ({@code String})</li> + * </ul> + */ + JAR_SIG_NO_ZIP_ENTRY_DIGEST_IN_MANIFEST("No digest for %1$s in META-INF/MANIFEST.MF"), + + /** + * JAR signature does not list a digest for the specified entry. + * + * <ul> + * <li>Parameter 1: entry name ({@code String})</li> + * <li>Parameter 2: signature file name ({@code String})</li> + * </ul> + */ + JAR_SIG_NO_ZIP_ENTRY_DIGEST_IN_SIG_FILE("No digest for %1$s in %2$s"), + + /** + * The specified JAR entry is not covered by JAR signature. + * + * <ul> + * <li>Parameter 1: entry name ({@code String})</li> + * </ul> + */ + JAR_SIG_ZIP_ENTRY_NOT_SIGNED("%1$s entry not signed"), + + /** + * JAR signature uses different set of signers to protect the two specified ZIP entries. + * + * <ul> + * <li>Parameter 1: first entry name ({@code String})</li> + * <li>Parameter 2: first entry signer names ({@code List<String>})</li> + * <li>Parameter 3: second entry name ({@code String})</li> + * <li>Parameter 4: second entry signer names ({@code List<String>})</li> + * </ul> + */ + JAR_SIG_ZIP_ENTRY_SIGNERS_MISMATCH( + "Entries %1$s and %3$s are signed with different sets of signers" + + " : <%2$s> vs <%4$s>"), + + /** + * Digest of the specified ZIP entry's data does not match the digest expected by the JAR + * signature. + * + * <ul> + * <li>Parameter 1: entry name ({@code String})</li> + * <li>Parameter 2: digest algorithm (e.g., SHA-256) ({@code String})</li> + * <li>Parameter 3: name of the entry in which the expected digest is specified + * ({@code String})</li> + * <li>Parameter 4: base64-encoded actual digest ({@code String})</li> + * <li>Parameter 5: base64-encoded expected digest ({@code String})</li> + * </ul> + */ + JAR_SIG_ZIP_ENTRY_DIGEST_DID_NOT_VERIFY( + "%2$s digest of %1$s does not match the digest specified in %3$s" + + ". Expected: <%5$s>, actual: <%4$s>"), + + /** + * Digest of the JAR manifest main section did not verify. + * + * <ul> + * <li>Parameter 1: digest algorithm (e.g., SHA-256) ({@code String})</li> + * <li>Parameter 2: name of the entry in which the expected digest is specified + * ({@code String})</li> + * <li>Parameter 3: base64-encoded actual digest ({@code String})</li> + * <li>Parameter 4: base64-encoded expected digest ({@code String})</li> + * </ul> + */ + JAR_SIG_MANIFEST_MAIN_SECTION_DIGEST_DID_NOT_VERIFY( + "%1$s digest of META-INF/MANIFEST.MF main section does not match the digest" + + " specified in %2$s. Expected: <%4$s>, actual: <%3$s>"), + + /** + * Digest of the specified JAR manifest section does not match the digest expected by the + * JAR signature. + * + * <ul> + * <li>Parameter 1: section name ({@code String})</li> + * <li>Parameter 2: digest algorithm (e.g., SHA-256) ({@code String})</li> + * <li>Parameter 3: name of the signature file in which the expected digest is specified + * ({@code String})</li> + * <li>Parameter 4: base64-encoded actual digest ({@code String})</li> + * <li>Parameter 5: base64-encoded expected digest ({@code String})</li> + * </ul> + */ + JAR_SIG_MANIFEST_SECTION_DIGEST_DID_NOT_VERIFY( + "%2$s digest of META-INF/MANIFEST.MF section for %1$s does not match the digest" + + " specified in %3$s. Expected: <%5$s>, actual: <%4$s>"), + + /** + * JAR signature file does not contain the whole-file digest of the JAR manifest file. The + * digest speeds up verification of JAR signature. + * + * <ul> + * <li>Parameter 1: name of the signature file ({@code String})</li> + * </ul> + */ + JAR_SIG_NO_MANIFEST_DIGEST_IN_SIG_FILE( + "%1$s does not specify digest of META-INF/MANIFEST.MF" + + ". This slows down verification."), + + /** + * APK is signed using APK Signature Scheme v2 or newer, but JAR signature file does not + * contain protections against stripping of these newer scheme signatures. + * + * <ul> + * <li>Parameter 1: name of the signature file ({@code String})</li> + * </ul> + */ + JAR_SIG_NO_APK_SIG_STRIP_PROTECTION( + "APK is signed using APK Signature Scheme v2 but these signatures may be stripped" + + " without being detected because %1$s does not contain anti-stripping" + + " protections."), + + /** + * JAR signature of the signer is missing a file/entry. + * + * <ul> + * <li>Parameter 1: name of the encountered file ({@code String})</li> + * <li>Parameter 2: name of the missing file ({@code String})</li> + * </ul> + */ + JAR_SIG_MISSING_FILE("Partial JAR signature. Found: %1$s, missing: %2$s"), + + /** + * An exception was encountered while verifying JAR signature contained in a signature block + * against the signature file. + * + * <ul> + * <li>Parameter 1: name of the signature block file ({@code String})</li> + * <li>Parameter 2: name of the signature file ({@code String})</li> + * <li>Parameter 3: exception ({@code Throwable})</li> + * </ul> + */ + JAR_SIG_VERIFY_EXCEPTION("Failed to verify JAR signature %1$s against %2$s: %3$s"), + + /** + * JAR signature contains unsupported digest algorithm. + * + * <ul> + * <li>Parameter 1: name of the signature block file ({@code String})</li> + * <li>Parameter 2: digest algorithm OID ({@code String})</li> + * <li>Parameter 3: signature algorithm OID ({@code String})</li> + * <li>Parameter 4: API Levels on which this combination of algorithms is not supported + * ({@code String})</li> + * <li>Parameter 5: user-friendly variant of digest algorithm ({@code String})</li> + * <li>Parameter 6: user-friendly variant of signature algorithm ({@code String})</li> + * </ul> + */ + JAR_SIG_UNSUPPORTED_SIG_ALG( + "JAR signature %1$s uses digest algorithm %5$s and signature algorithm %6$s which" + + " is not supported on API Level(s) %4$s for which this APK is being" + + " verified"), + + /** + * An exception was encountered while parsing JAR signature contained in a signature block. + * + * <ul> + * <li>Parameter 1: name of the signature block file ({@code String})</li> + * <li>Parameter 2: exception ({@code Throwable})</li> + * </ul> + */ + JAR_SIG_PARSE_EXCEPTION("Failed to parse JAR signature %1$s: %2$s"), + + /** + * An exception was encountered while parsing a certificate contained in the JAR signature + * block. + * + * <ul> + * <li>Parameter 1: name of the signature block file ({@code String})</li> + * <li>Parameter 2: exception ({@code Throwable})</li> + * </ul> + */ + JAR_SIG_MALFORMED_CERTIFICATE("Malformed certificate in JAR signature %1$s: %2$s"), + + /** + * JAR signature contained in a signature block file did not verify against the signature + * file. + * + * <ul> + * <li>Parameter 1: name of the signature block file ({@code String})</li> + * <li>Parameter 2: name of the signature file ({@code String})</li> + * </ul> + */ + JAR_SIG_DID_NOT_VERIFY("JAR signature %1$s did not verify against %2$s"), + + /** + * JAR signature contains no verified signers. + * + * <ul> + * <li>Parameter 1: name of the signature block file ({@code String})</li> + * </ul> + */ + JAR_SIG_NO_SIGNERS("JAR signature %1$s contains no signers"), + + /** + * JAR signature file contains a section with a duplicate name. + * + * <ul> + * <li>Parameter 1: signature file name ({@code String})</li> + * <li>Parameter 1: section name ({@code String})</li> + * </ul> + */ + JAR_SIG_DUPLICATE_SIG_FILE_SECTION("Duplicate section in %1$s: %2$s"), + + /** + * JAR signature file's main section doesn't contain the mandatory Signature-Version + * attribute. + * + * <ul> + * <li>Parameter 1: signature file name ({@code String})</li> + * </ul> + */ + JAR_SIG_MISSING_VERSION_ATTR_IN_SIG_FILE( + "Malformed %1$s: missing Signature-Version attribute"), + + /** + * JAR signature file references an unknown APK signature scheme ID. + * + * <ul> + * <li>Parameter 1: name of the signature file ({@code String})</li> + * <li>Parameter 2: unknown APK signature scheme ID ({@code} Integer)</li> + * </ul> + */ + JAR_SIG_UNKNOWN_APK_SIG_SCHEME_ID( + "JAR signature %1$s references unknown APK signature scheme ID: %2$d"), + + /** + * JAR signature file indicates that the APK is supposed to be signed with a supported APK + * signature scheme (in addition to the JAR signature) but no such signature was found in + * the APK. + * + * <ul> + * <li>Parameter 1: name of the signature file ({@code String})</li> + * <li>Parameter 2: APK signature scheme ID ({@code} Integer)</li> + * <li>Parameter 3: APK signature scheme English name ({@code} String)</li> + * </ul> + */ + JAR_SIG_MISSING_APK_SIG_REFERENCED( + "JAR signature %1$s indicates the APK is signed using %3$s but no such signature" + + " was found. Signature stripped?"), + + /** + * JAR entry is not covered by signature and thus unauthorized modifications to its contents + * will not be detected. + * + * <ul> + * <li>Parameter 1: entry name ({@code String})</li> + * </ul> + */ + JAR_SIG_UNPROTECTED_ZIP_ENTRY( + "%1$s not protected by signature. Unauthorized modifications to this JAR entry" + + " will not be detected. Delete or move the entry outside of META-INF/."), + + /** + * APK which is both JAR-signed and signed using APK Signature Scheme v2 contains an APK + * Signature Scheme v2 signature from this signer, but does not contain a JAR signature + * from this signer. + */ + JAR_SIG_MISSING("No JAR signature from this signer"), + + /** + * APK is targeting a sandbox version which requires APK Signature Scheme v2 signature but + * no such signature was found. + * + * <ul> + * <li>Parameter 1: target sandbox version ({@code Integer})</li> + * </ul> + */ + NO_SIG_FOR_TARGET_SANDBOX_VERSION( + "Missing APK Signature Scheme v2 signature required for target sandbox version" + + " %1$d"), + + /** + * APK is targeting an SDK version that requires a minimum signature scheme version, but the + * APK is not signed with that version or later. + * + * <ul> + * <li>Parameter 1: target SDK Version (@code Integer})</li> + * <li>Parameter 2: minimum signature scheme version ((@code Integer})</li> + * </ul> + */ + MIN_SIG_SCHEME_FOR_TARGET_SDK_NOT_MET( + "Target SDK version %1$d requires a minimum of signature scheme v%2$d; the APK is" + + " not signed with this or a later signature scheme"), + + /** + * APK which is both JAR-signed and signed using APK Signature Scheme v2 contains a JAR + * signature from this signer, but does not contain an APK Signature Scheme v2 signature + * from this signer. + */ + V2_SIG_MISSING("No APK Signature Scheme v2 signature from this signer"), + + /** + * Failed to parse the list of signers contained in the APK Signature Scheme v2 signature. + */ + V2_SIG_MALFORMED_SIGNERS("Malformed list of signers"), + + /** + * Failed to parse this signer's signer block contained in the APK Signature Scheme v2 + * signature. + */ + V2_SIG_MALFORMED_SIGNER("Malformed signer block"), + + /** + * Public key embedded in the APK Signature Scheme v2 signature of this signer could not be + * parsed. + * + * <ul> + * <li>Parameter 1: error details ({@code Throwable})</li> + * </ul> + */ + V2_SIG_MALFORMED_PUBLIC_KEY("Malformed public key: %1$s"), + + /** + * This APK Signature Scheme v2 signer's certificate could not be parsed. + * + * <ul> + * <li>Parameter 1: index ({@code 0}-based) of the certificate in the signer's list of + * certificates ({@code Integer})</li> + * <li>Parameter 2: sequence number ({@code 1}-based) of the certificate in the signer's + * list of certificates ({@code Integer})</li> + * <li>Parameter 3: error details ({@code Throwable})</li> + * </ul> + */ + V2_SIG_MALFORMED_CERTIFICATE("Malformed certificate #%2$d: %3$s"), + + /** + * Failed to parse this signer's signature record contained in the APK Signature Scheme v2 + * signature. + * + * <ul> + * <li>Parameter 1: record number (first record is {@code 1}) ({@code Integer})</li> + * </ul> + */ + V2_SIG_MALFORMED_SIGNATURE("Malformed APK Signature Scheme v2 signature record #%1$d"), + + /** + * Failed to parse this signer's digest record contained in the APK Signature Scheme v2 + * signature. + * + * <ul> + * <li>Parameter 1: record number (first record is {@code 1}) ({@code Integer})</li> + * </ul> + */ + V2_SIG_MALFORMED_DIGEST("Malformed APK Signature Scheme v2 digest record #%1$d"), + + /** + * This APK Signature Scheme v2 signer contains a malformed additional attribute. + * + * <ul> + * <li>Parameter 1: attribute number (first attribute is {@code 1}) {@code Integer})</li> + * </ul> + */ + V2_SIG_MALFORMED_ADDITIONAL_ATTRIBUTE("Malformed additional attribute #%1$d"), + + /** + * APK Signature Scheme v2 signature references an unknown APK signature scheme ID. + * + * <ul> + * <li>Parameter 1: signer index ({@code Integer})</li> + * <li>Parameter 2: unknown APK signature scheme ID ({@code} Integer)</li> + * </ul> + */ + V2_SIG_UNKNOWN_APK_SIG_SCHEME_ID( + "APK Signature Scheme v2 signer: %1$s references unknown APK signature scheme ID: " + + "%2$d"), + + /** + * APK Signature Scheme v2 signature indicates that the APK is supposed to be signed with a + * supported APK signature scheme (in addition to the v2 signature) but no such signature + * was found in the APK. + * + * <ul> + * <li>Parameter 1: signer index ({@code Integer})</li> + * <li>Parameter 2: APK signature scheme English name ({@code} String)</li> + * </ul> + */ + V2_SIG_MISSING_APK_SIG_REFERENCED( + "APK Signature Scheme v2 signature %1$s indicates the APK is signed using %2$s but " + + "no such signature was found. Signature stripped?"), + + /** + * APK signature scheme v2 has exceeded the maximum number of signers. + * <ul> + * <li>Parameter 1: maximum allowed signers ({@code Integer})</li> + * <li>Parameter 2: total number of signers ({@code Integer})</li> + * </ul> + */ + V2_SIG_MAX_SIGNATURES_EXCEEDED( + "APK Signature Scheme V2 only supports a maximum of %1$d signers, found %2$d"), + + /** + * APK Signature Scheme v2 signature contains no signers. + */ + V2_SIG_NO_SIGNERS("No signers in APK Signature Scheme v2 signature"), + + /** + * This APK Signature Scheme v2 signer contains a signature produced using an unknown + * algorithm. + * + * <ul> + * <li>Parameter 1: algorithm ID ({@code Integer})</li> + * </ul> + */ + V2_SIG_UNKNOWN_SIG_ALGORITHM("Unknown signature algorithm: %1$#x"), + + /** + * This APK Signature Scheme v2 signer contains an unknown additional attribute. + * + * <ul> + * <li>Parameter 1: attribute ID ({@code Integer})</li> + * </ul> + */ + V2_SIG_UNKNOWN_ADDITIONAL_ATTRIBUTE("Unknown additional attribute: ID %1$#x"), + + /** + * An exception was encountered while verifying APK Signature Scheme v2 signature of this + * signer. + * + * <ul> + * <li>Parameter 1: signature algorithm ({@link SignatureAlgorithm})</li> + * <li>Parameter 2: exception ({@code Throwable})</li> + * </ul> + */ + V2_SIG_VERIFY_EXCEPTION("Failed to verify %1$s signature: %2$s"), + + /** + * APK Signature Scheme v2 signature over this signer's signed-data block did not verify. + * + * <ul> + * <li>Parameter 1: signature algorithm ({@link SignatureAlgorithm})</li> + * </ul> + */ + V2_SIG_DID_NOT_VERIFY("%1$s signature over signed-data did not verify"), + + /** + * This APK Signature Scheme v2 signer offers no signatures. + */ + V2_SIG_NO_SIGNATURES("No signatures"), + + /** + * This APK Signature Scheme v2 signer offers signatures but none of them are supported. + */ + V2_SIG_NO_SUPPORTED_SIGNATURES("No supported signatures: %1$s"), + + /** + * This APK Signature Scheme v2 signer offers no certificates. + */ + V2_SIG_NO_CERTIFICATES("No certificates"), + + /** + * This APK Signature Scheme v2 signer's public key listed in the signer's certificate does + * not match the public key listed in the signatures record. + * + * <ul> + * <li>Parameter 1: hex-encoded public key from certificate ({@code String})</li> + * <li>Parameter 2: hex-encoded public key from signatures record ({@code String})</li> + * </ul> + */ + V2_SIG_PUBLIC_KEY_MISMATCH_BETWEEN_CERTIFICATE_AND_SIGNATURES_RECORD( + "Public key mismatch between certificate and signature record: <%1$s> vs <%2$s>"), + + /** + * This APK Signature Scheme v2 signer's signature algorithms listed in the signatures + * record do not match the signature algorithms listed in the signatures record. + * + * <ul> + * <li>Parameter 1: signature algorithms from signatures record ({@code List<Integer>})</li> + * <li>Parameter 2: signature algorithms from digests record ({@code List<Integer>})</li> + * </ul> + */ + V2_SIG_SIG_ALG_MISMATCH_BETWEEN_SIGNATURES_AND_DIGESTS_RECORDS( + "Signature algorithms mismatch between signatures and digests records" + + ": %1$s vs %2$s"), + + /** + * The APK's digest does not match the digest contained in the APK Signature Scheme v2 + * signature. + * + * <ul> + * <li>Parameter 1: content digest algorithm ({@link ContentDigestAlgorithm})</li> + * <li>Parameter 2: hex-encoded expected digest of the APK ({@code String})</li> + * <li>Parameter 3: hex-encoded actual digest of the APK ({@code String})</li> + * </ul> + */ + V2_SIG_APK_DIGEST_DID_NOT_VERIFY( + "APK integrity check failed. %1$s digest mismatch." + + " Expected: <%2$s>, actual: <%3$s>"), + + /** + * Failed to parse the list of signers contained in the APK Signature Scheme v3 signature. + */ + V3_SIG_MALFORMED_SIGNERS("Malformed list of signers"), + + /** + * Failed to parse this signer's signer block contained in the APK Signature Scheme v3 + * signature. + */ + V3_SIG_MALFORMED_SIGNER("Malformed signer block"), + + /** + * Public key embedded in the APK Signature Scheme v3 signature of this signer could not be + * parsed. + * + * <ul> + * <li>Parameter 1: error details ({@code Throwable})</li> + * </ul> + */ + V3_SIG_MALFORMED_PUBLIC_KEY("Malformed public key: %1$s"), + + /** + * This APK Signature Scheme v3 signer's certificate could not be parsed. + * + * <ul> + * <li>Parameter 1: index ({@code 0}-based) of the certificate in the signer's list of + * certificates ({@code Integer})</li> + * <li>Parameter 2: sequence number ({@code 1}-based) of the certificate in the signer's + * list of certificates ({@code Integer})</li> + * <li>Parameter 3: error details ({@code Throwable})</li> + * </ul> + */ + V3_SIG_MALFORMED_CERTIFICATE("Malformed certificate #%2$d: %3$s"), + + /** + * Failed to parse this signer's signature record contained in the APK Signature Scheme v3 + * signature. + * + * <ul> + * <li>Parameter 1: record number (first record is {@code 1}) ({@code Integer})</li> + * </ul> + */ + V3_SIG_MALFORMED_SIGNATURE("Malformed APK Signature Scheme v3 signature record #%1$d"), + + /** + * Failed to parse this signer's digest record contained in the APK Signature Scheme v3 + * signature. + * + * <ul> + * <li>Parameter 1: record number (first record is {@code 1}) ({@code Integer})</li> + * </ul> + */ + V3_SIG_MALFORMED_DIGEST("Malformed APK Signature Scheme v3 digest record #%1$d"), + + /** + * This APK Signature Scheme v3 signer contains a malformed additional attribute. + * + * <ul> + * <li>Parameter 1: attribute number (first attribute is {@code 1}) {@code Integer})</li> + * </ul> + */ + V3_SIG_MALFORMED_ADDITIONAL_ATTRIBUTE("Malformed additional attribute #%1$d"), + + /** + * APK Signature Scheme v3 signature contains no signers. + */ + V3_SIG_NO_SIGNERS("No signers in APK Signature Scheme v3 signature"), + + /** + * APK Signature Scheme v3 signature contains multiple signers (only one allowed per + * platform version). + */ + V3_SIG_MULTIPLE_SIGNERS("Multiple APK Signature Scheme v3 signatures found for a single " + + " platform version."), + + /** + * APK Signature Scheme v3 signature found, but multiple v1 and/or multiple v2 signers + * found, where only one may be used with APK Signature Scheme v3 + */ + V3_SIG_MULTIPLE_PAST_SIGNERS("Multiple signatures found for pre-v3 signing with an APK " + + " Signature Scheme v3 signer. Only one allowed."), + + /** + * APK Signature Scheme v3 signature found, but its signer doesn't match the v1/v2 signers, + * or have them as the root of its signing certificate history + */ + V3_SIG_PAST_SIGNERS_MISMATCH( + "v3 signer differs from v1/v2 signer without proper signing certificate lineage."), + + /** + * This APK Signature Scheme v3 signer contains a signature produced using an unknown + * algorithm. + * + * <ul> + * <li>Parameter 1: algorithm ID ({@code Integer})</li> + * </ul> + */ + V3_SIG_UNKNOWN_SIG_ALGORITHM("Unknown signature algorithm: %1$#x"), + + /** + * This APK Signature Scheme v3 signer contains an unknown additional attribute. + * + * <ul> + * <li>Parameter 1: attribute ID ({@code Integer})</li> + * </ul> + */ + V3_SIG_UNKNOWN_ADDITIONAL_ATTRIBUTE("Unknown additional attribute: ID %1$#x"), + + /** + * An exception was encountered while verifying APK Signature Scheme v3 signature of this + * signer. + * + * <ul> + * <li>Parameter 1: signature algorithm ({@link SignatureAlgorithm})</li> + * <li>Parameter 2: exception ({@code Throwable})</li> + * </ul> + */ + V3_SIG_VERIFY_EXCEPTION("Failed to verify %1$s signature: %2$s"), + + /** + * The APK Signature Scheme v3 signer contained an invalid value for either min or max SDK + * versions. + * + * <ul> + * <li>Parameter 1: minSdkVersion ({@code Integer}) + * <li>Parameter 2: maxSdkVersion ({@code Integer}) + * </ul> + */ + V3_SIG_INVALID_SDK_VERSIONS("Invalid SDK Version parameter(s) encountered in APK Signature " + + "scheme v3 signature: minSdkVersion %1$s maxSdkVersion: %2$s"), + + /** + * APK Signature Scheme v3 signature over this signer's signed-data block did not verify. + * + * <ul> + * <li>Parameter 1: signature algorithm ({@link SignatureAlgorithm})</li> + * </ul> + */ + V3_SIG_DID_NOT_VERIFY("%1$s signature over signed-data did not verify"), + + /** + * This APK Signature Scheme v3 signer offers no signatures. + */ + V3_SIG_NO_SIGNATURES("No signatures"), + + /** + * This APK Signature Scheme v3 signer offers signatures but none of them are supported. + */ + V3_SIG_NO_SUPPORTED_SIGNATURES("No supported signatures"), + + /** + * This APK Signature Scheme v3 signer offers no certificates. + */ + V3_SIG_NO_CERTIFICATES("No certificates"), + + /** + * This APK Signature Scheme v3 signer's minSdkVersion listed in the signer's signed data + * does not match the minSdkVersion listed in the signatures record. + * + * <ul> + * <li>Parameter 1: minSdkVersion in signature record ({@code Integer}) </li> + * <li>Parameter 2: minSdkVersion in signed data ({@code Integer}) </li> + * </ul> + */ + V3_MIN_SDK_VERSION_MISMATCH_BETWEEN_SIGNER_AND_SIGNED_DATA_RECORD( + "minSdkVersion mismatch between signed data and signature record:" + + " <%1$s> vs <%2$s>"), + + /** + * This APK Signature Scheme v3 signer's maxSdkVersion listed in the signer's signed data + * does not match the maxSdkVersion listed in the signatures record. + * + * <ul> + * <li>Parameter 1: maxSdkVersion in signature record ({@code Integer}) </li> + * <li>Parameter 2: maxSdkVersion in signed data ({@code Integer}) </li> + * </ul> + */ + V3_MAX_SDK_VERSION_MISMATCH_BETWEEN_SIGNER_AND_SIGNED_DATA_RECORD( + "maxSdkVersion mismatch between signed data and signature record:" + + " <%1$s> vs <%2$s>"), + + /** + * This APK Signature Scheme v3 signer's public key listed in the signer's certificate does + * not match the public key listed in the signatures record. + * + * <ul> + * <li>Parameter 1: hex-encoded public key from certificate ({@code String})</li> + * <li>Parameter 2: hex-encoded public key from signatures record ({@code String})</li> + * </ul> + */ + V3_SIG_PUBLIC_KEY_MISMATCH_BETWEEN_CERTIFICATE_AND_SIGNATURES_RECORD( + "Public key mismatch between certificate and signature record: <%1$s> vs <%2$s>"), + + /** + * This APK Signature Scheme v3 signer's signature algorithms listed in the signatures + * record do not match the signature algorithms listed in the signatures record. + * + * <ul> + * <li>Parameter 1: signature algorithms from signatures record ({@code List<Integer>})</li> + * <li>Parameter 2: signature algorithms from digests record ({@code List<Integer>})</li> + * </ul> + */ + V3_SIG_SIG_ALG_MISMATCH_BETWEEN_SIGNATURES_AND_DIGESTS_RECORDS( + "Signature algorithms mismatch between signatures and digests records" + + ": %1$s vs %2$s"), + + /** + * The APK's digest does not match the digest contained in the APK Signature Scheme v3 + * signature. + * + * <ul> + * <li>Parameter 1: content digest algorithm ({@link ContentDigestAlgorithm})</li> + * <li>Parameter 2: hex-encoded expected digest of the APK ({@code String})</li> + * <li>Parameter 3: hex-encoded actual digest of the APK ({@code String})</li> + * </ul> + */ + V3_SIG_APK_DIGEST_DID_NOT_VERIFY( + "APK integrity check failed. %1$s digest mismatch." + + " Expected: <%2$s>, actual: <%3$s>"), + + /** + * The signer's SigningCertificateLineage attribute containd a proof-of-rotation record with + * signature(s) that did not verify. + */ + V3_SIG_POR_DID_NOT_VERIFY("SigningCertificateLineage attribute containd a proof-of-rotation" + + " record with signature(s) that did not verify."), + + /** + * Failed to parse the SigningCertificateLineage structure in the APK Signature Scheme v3 + * signature's additional attributes section. + */ + V3_SIG_MALFORMED_LINEAGE("Failed to parse the SigningCertificateLineage structure in the " + + "APK Signature Scheme v3 signature's additional attributes section."), + + /** + * The APK's signing certificate does not match the terminal node in the provided + * proof-of-rotation structure describing the signing certificate history + */ + V3_SIG_POR_CERT_MISMATCH( + "APK signing certificate differs from the associated certificate found in the " + + "signer's SigningCertificateLineage."), + + /** + * The APK Signature Scheme v3 signers encountered do not offer a continuous set of + * supported platform versions. Either they overlap, resulting in potentially two + * acceptable signers for a platform version, or there are holes which would create problems + * in the event of platform version upgrades. + */ + V3_INCONSISTENT_SDK_VERSIONS("APK Signature Scheme v3 signers supported min/max SDK " + + "versions are not continuous."), + + /** + * The APK Signature Scheme v3 signers don't cover all requested SDK versions. + * + * <ul> + * <li>Parameter 1: minSdkVersion ({@code Integer}) + * <li>Parameter 2: maxSdkVersion ({@code Integer}) + * </ul> + */ + V3_MISSING_SDK_VERSIONS("APK Signature Scheme v3 signers supported min/max SDK " + + "versions do not cover the entire desired range. Found min: %1$s max %2$s"), + + /** + * The SigningCertificateLineages for different platform versions using APK Signature Scheme + * v3 do not go together. Specifically, each should be a subset of another, with the size + * of each increasing as the platform level increases. + */ + V3_INCONSISTENT_LINEAGES("SigningCertificateLineages targeting different platform versions" + + " using APK Signature Scheme v3 are not all a part of the same overall lineage."), + + /** + * The v3 stripping protection attribute for rotation is present, but a v3.1 signing block + * was not found. + * + * <ul> + * <li>Parameter 1: min SDK version supporting rotation from attribute ({@code Integer}) + * </ul> + */ + V31_BLOCK_MISSING( + "The v3 signer indicates key rotation should be supported starting from SDK " + + "version %1$s, but a v3.1 block was not found"), + + /** + * The v3 stripping protection attribute for rotation does not match the minimum SDK version + * targeting rotation in the v3.1 signer block. + * + * <ul> + * <li>Parameter 1: min SDK version supporting rotation from attribute ({@code Integer}) + * <li>Parameter 2: min SDK version supporting rotation from v3.1 block ({@code Integer}) + * </ul> + */ + V31_ROTATION_MIN_SDK_MISMATCH( + "The v3 signer indicates key rotation should be supported starting from SDK " + + "version %1$s, but the v3.1 block targets %2$s for rotation"), + + /** + * The APK supports key rotation with SDK version targeting using v3.1, but the rotation min + * SDK version stripping protection attribute was not written to the v3 signer. + * + * <ul> + * <li>Parameter 1: min SDK version supporting rotation from v3.1 block ({@code Integer}) + * </ul> + */ + V31_ROTATION_MIN_SDK_ATTR_MISSING( + "APK supports key rotation starting from SDK version %1$s, but the v3 signer does" + + " not contain the attribute to detect if this signature is stripped"), + + /** + * The APK contains a v3.1 signing block without a v3.0 block. The v3.1 block should only + * be used for targeting rotation for a later SDK version; if an APK's minSdkVersion is the + * same as the SDK version for rotation then this should be written to a v3.0 block. + */ + V31_BLOCK_FOUND_WITHOUT_V3_BLOCK( + "The APK contains a v3.1 signing block without a v3.0 base block"), + + /** + * The APK contains a v3.0 signing block with a rotation-targets-dev-release attribute in + * the signer; this attribute is only intended for v3.1 signers to indicate they should be + * targeting the next development release that is using the SDK version of the previously + * released platform SDK version. + */ + V31_ROTATION_TARGETS_DEV_RELEASE_ATTR_ON_V3_SIGNER( + "The rotation-targets-dev-release attribute is only supported on v3.1 signers; " + + "this attribute will be ignored by the platform in a v3.0 signer"), + + /** + * APK Signing Block contains an unknown entry. + * + * <ul> + * <li>Parameter 1: entry ID ({@code Integer})</li> + * </ul> + */ + APK_SIG_BLOCK_UNKNOWN_ENTRY_ID("APK Signing Block contains unknown entry: ID %1$#x"), + + /** + * Failed to parse this signer's signature record contained in the APK Signature Scheme + * V4 signature. + * + * <ul> + * <li>Parameter 1: record number (first record is {@code 1}) ({@code Integer})</li> + * </ul> + */ + V4_SIG_MALFORMED_SIGNERS( + "V4 signature has malformed signer block"), + + /** + * This APK Signature Scheme V4 signer contains a signature produced using an + * unknown algorithm. + * + * <ul> + * <li>Parameter 1: algorithm ID ({@code Integer})</li> + * </ul> + */ + V4_SIG_UNKNOWN_SIG_ALGORITHM( + "V4 signature has unknown signing algorithm: %1$#x"), + + /** + * This APK Signature Scheme V4 signer offers no signatures. + */ + V4_SIG_NO_SIGNATURES( + "V4 signature has no signature found"), + + /** + * This APK Signature Scheme V4 signer offers signatures but none of them are + * supported. + */ + V4_SIG_NO_SUPPORTED_SIGNATURES( + "V4 signature has no supported signature"), + + /** + * APK Signature Scheme v3 signature over this signer's signed-data block did not verify. + * + * <ul> + * <li>Parameter 1: signature algorithm ({@link SignatureAlgorithm})</li> + * </ul> + */ + V4_SIG_DID_NOT_VERIFY("%1$s signature over signed-data did not verify"), + + /** + * An exception was encountered while verifying APK Signature Scheme v3 signature of this + * signer. + * + * <ul> + * <li>Parameter 1: signature algorithm ({@link SignatureAlgorithm})</li> + * <li>Parameter 2: exception ({@code Throwable})</li> + * </ul> + */ + V4_SIG_VERIFY_EXCEPTION("Failed to verify %1$s signature: %2$s"), + + /** + * Public key embedded in the APK Signature Scheme v4 signature of this signer could not be + * parsed. + * + * <ul> + * <li>Parameter 1: error details ({@code Throwable})</li> + * </ul> + */ + V4_SIG_MALFORMED_PUBLIC_KEY("Malformed public key: %1$s"), + + /** + * This APK Signature Scheme V4 signer's certificate could not be parsed. + * + * <ul> + * <li>Parameter 1: index ({@code 0}-based) of the certificate in the signer's list of + * certificates ({@code Integer})</li> + * <li>Parameter 2: sequence number ({@code 1}-based) of the certificate in the signer's + * list of certificates ({@code Integer})</li> + * <li>Parameter 3: error details ({@code Throwable})</li> + * </ul> + */ + V4_SIG_MALFORMED_CERTIFICATE( + "V4 signature has malformed certificate"), + + /** + * This APK Signature Scheme V4 signer offers no certificate. + */ + V4_SIG_NO_CERTIFICATE("V4 signature has no certificate"), + + /** + * This APK Signature Scheme V4 signer's public key listed in the signer's + * certificate does not match the public key listed in the signature proto. + * + * <ul> + * <li>Parameter 1: hex-encoded public key from certificate ({@code String})</li> + * <li>Parameter 2: hex-encoded public key from signature proto ({@code String})</li> + * </ul> + */ + V4_SIG_PUBLIC_KEY_MISMATCH_BETWEEN_CERTIFICATE_AND_SIGNATURES_RECORD( + "V4 signature has mismatched certificate and signature: <%1$s> vs <%2$s>"), + + /** + * The APK's hash root (aka digest) does not match the hash root contained in the Signature + * Scheme V4 signature. + * + * <ul> + * <li>Parameter 1: content digest algorithm ({@link ContentDigestAlgorithm})</li> + * <li>Parameter 2: hex-encoded expected digest of the APK ({@code String})</li> + * <li>Parameter 3: hex-encoded actual digest of the APK ({@code String})</li> + * </ul> + */ + V4_SIG_APK_ROOT_DID_NOT_VERIFY( + "V4 signature's hash tree root (content digest) did not verity"), + + /** + * The APK's hash tree does not match the hash tree contained in the Signature + * Scheme V4 signature. + * + * <ul> + * <li>Parameter 1: content digest algorithm ({@link ContentDigestAlgorithm})</li> + * <li>Parameter 2: hex-encoded expected hash tree of the APK ({@code String})</li> + * <li>Parameter 3: hex-encoded actual hash tree of the APK ({@code String})</li> + * </ul> + */ + V4_SIG_APK_TREE_DID_NOT_VERIFY( + "V4 signature's hash tree did not verity"), + + /** + * Using more than one Signer to sign APK Signature Scheme V4 signature. + */ + V4_SIG_MULTIPLE_SIGNERS( + "V4 signature only supports one signer"), + + /** + * V4.1 signature requires two signers to match the v3 and the v3.1. + */ + V41_SIG_NEEDS_TWO_SIGNERS("V4.1 signature requires two signers"), + + /** + * The signer used to sign APK Signature Scheme V2/V3 signature does not match the signer + * used to sign APK Signature Scheme V4 signature. + */ + V4_SIG_V2_V3_SIGNERS_MISMATCH( + "V4 signature and V2/V3 signature have mismatched certificates"), + + /** + * The v4 signature's digest does not match the digest from the corresponding v2 / v3 + * signature. + * + * <ul> + * <li>Parameter 1: Signature scheme of mismatched digest ({@code int}) + * <li>Parameter 2: v2/v3 digest ({@code String}) + * <li>Parameter 3: v4 digest ({@code String}) + * </ul> + */ + V4_SIG_V2_V3_DIGESTS_MISMATCH( + "V4 signature and V%1$d signature have mismatched digests, V%1$d digest: %2$s, V4" + + " digest: %3$s"), + + /** + * The v4 signature does not contain the expected number of digests. + * + * <ul> + * <li>Parameter 1: Number of digests found ({@code int}) + * </ul> + */ + V4_SIG_UNEXPECTED_DIGESTS( + "V4 signature does not have the expected number of digests, found %1$d"), + + /** + * The v4 signature format version isn't the same as the tool's current version, something + * may go wrong. + */ + V4_SIG_VERSION_NOT_CURRENT( + "V4 signature format version %1$d is different from the tool's current " + + "version %2$d"), + + /** + * The APK does not contain the source stamp certificate digest file nor the signature block + * when verification expected a source stamp to be present. + */ + SOURCE_STAMP_CERT_DIGEST_AND_SIG_BLOCK_MISSING( + "Neither the source stamp certificate digest file nor the signature block are " + + "present in the APK"), + + /** APK contains SourceStamp file, but does not contain a SourceStamp signature. */ + SOURCE_STAMP_SIG_MISSING("No SourceStamp signature"), + + /** + * SourceStamp's certificate could not be parsed. + * + * <ul> + * <li>Parameter 1: error details ({@code Throwable}) + * </ul> + */ + SOURCE_STAMP_MALFORMED_CERTIFICATE("Malformed certificate: %1$s"), + + /** Failed to parse SourceStamp's signature. */ + SOURCE_STAMP_MALFORMED_SIGNATURE("Malformed SourceStamp signature"), + + /** + * SourceStamp contains a signature produced using an unknown algorithm. + * + * <ul> + * <li>Parameter 1: algorithm ID ({@code Integer}) + * </ul> + */ + SOURCE_STAMP_UNKNOWN_SIG_ALGORITHM("Unknown signature algorithm: %1$#x"), + + /** + * An exception was encountered while verifying SourceStamp signature. + * + * <ul> + * <li>Parameter 1: signature algorithm ({@link SignatureAlgorithm}) + * <li>Parameter 2: exception ({@code Throwable}) + * </ul> + */ + SOURCE_STAMP_VERIFY_EXCEPTION("Failed to verify %1$s signature: %2$s"), + + /** + * SourceStamp signature block did not verify. + * + * <ul> + * <li>Parameter 1: signature algorithm ({@link SignatureAlgorithm}) + * </ul> + */ + SOURCE_STAMP_DID_NOT_VERIFY("%1$s signature over signed-data did not verify"), + + /** SourceStamp offers no signatures. */ + SOURCE_STAMP_NO_SIGNATURE("No signature"), + + /** + * SourceStamp offers an unsupported signature. + * <ul> + * <li>Parameter 1: list of {@link SignatureAlgorithm}s in the source stamp + * signing block. + * <li>Parameter 2: {@code Exception} caught when attempting to obtain the list of + * supported signatures. + * </ul> + */ + SOURCE_STAMP_NO_SUPPORTED_SIGNATURE("Signature(s) {%1$s} not supported: %2$s"), + + /** + * SourceStamp's certificate listed in the APK signing block does not match the certificate + * listed in the SourceStamp file in the APK. + * + * <ul> + * <li>Parameter 1: SHA-256 hash of certificate from SourceStamp block in APK signing + * block ({@code String}) + * <li>Parameter 2: SHA-256 hash of certificate from SourceStamp file in APK ({@code + * String}) + * </ul> + */ + SOURCE_STAMP_CERTIFICATE_MISMATCH_BETWEEN_SIGNATURE_BLOCK_AND_APK( + "Certificate mismatch between SourceStamp block in APK signing block and" + + " SourceStamp file in APK: <%1$s> vs <%2$s>"), + + /** + * The APK contains a source stamp signature block without the expected certificate digest + * in the APK contents. + */ + SOURCE_STAMP_SIGNATURE_BLOCK_WITHOUT_CERT_DIGEST( + "A source stamp signature block was found without a corresponding certificate " + + "digest in the APK"), + + /** + * When verifying just the source stamp, the certificate digest in the APK does not match + * the expected digest. + * <ul> + * <li>Parameter 1: SHA-256 digest of the source stamp certificate in the APK. + * <li>Parameter 2: SHA-256 digest of the expected source stamp certificate. + * </ul> + */ + SOURCE_STAMP_EXPECTED_DIGEST_MISMATCH( + "The source stamp certificate digest in the APK, %1$s, does not match the " + + "expected digest, %2$s"), + + /** + * Source stamp block contains a malformed attribute. + * + * <ul> + * <li>Parameter 1: attribute number (first attribute is {@code 1}) {@code Integer})</li> + * </ul> + */ + SOURCE_STAMP_MALFORMED_ATTRIBUTE("Malformed stamp attribute #%1$d"), + + /** + * Source stamp block contains an unknown attribute. + * + * <ul> + * <li>Parameter 1: attribute ID ({@code Integer})</li> + * </ul> + */ + SOURCE_STAMP_UNKNOWN_ATTRIBUTE("Unknown stamp attribute: ID %1$#x"), + + /** + * Failed to parse the SigningCertificateLineage structure in the source stamp + * attributes section. + */ + SOURCE_STAMP_MALFORMED_LINEAGE("Failed to parse the SigningCertificateLineage " + + "structure in the source stamp attributes section."), + + /** + * The source stamp certificate does not match the terminal node in the provided + * proof-of-rotation structure describing the stamp certificate history. + */ + SOURCE_STAMP_POR_CERT_MISMATCH( + "APK signing certificate differs from the associated certificate found in the " + + "signer's SigningCertificateLineage."), + + /** + * The source stamp SigningCertificateLineage attribute contains a proof-of-rotation record + * with signature(s) that did not verify. + */ + SOURCE_STAMP_POR_DID_NOT_VERIFY("Source stamp SigningCertificateLineage attribute " + + "contains a proof-of-rotation record with signature(s) that did not verify."), + + /** + * The source stamp timestamp attribute has an invalid value (<= 0). + * <ul> + * <li>Parameter 1: The invalid timestamp value. + * </ul> + */ + SOURCE_STAMP_INVALID_TIMESTAMP( + "The source stamp" + + " timestamp attribute has an invalid value: %1$d"), + + /** + * The APK could not be properly parsed due to a ZIP or APK format exception. + * <ul> + * <li>Parameter 1: The {@code Exception} caught when attempting to parse the APK. + * </ul> + */ + MALFORMED_APK( + "Malformed APK; the following exception was caught when attempting to parse the " + + "APK: %1$s"), + + /** + * An unexpected exception was caught when attempting to verify the signature(s) within the + * APK. + * <ul> + * <li>Parameter 1: The {@code Exception} caught during verification. + * </ul> + */ + UNEXPECTED_EXCEPTION( + "An unexpected exception was caught when verifying the signature: %1$s"); + + private final String mFormat; + + Issue(String format) { + mFormat = format; + } + + /** + * Returns the format string suitable for combining the parameters of this issue into a + * readable string. See {@link java.util.Formatter} for format. + */ + private String getFormat() { + return mFormat; + } + } + + /** + * {@link Issue} with associated parameters. {@link #toString()} produces a readable formatted + * form. + */ + public static class IssueWithParams extends ApkVerificationIssue { + private final Issue mIssue; + private final Object[] mParams; + + /** + * Constructs a new {@code IssueWithParams} of the specified type and with provided + * parameters. + */ + public IssueWithParams(Issue issue, Object[] params) { + super(issue.mFormat, params); + mIssue = issue; + mParams = params; + } + + /** + * Returns the type of this issue. + */ + public Issue getIssue() { + return mIssue; + } + + /** + * Returns the parameters of this issue. + */ + public Object[] getParams() { + return mParams.clone(); + } + + /** + * Returns a readable form of this issue. + */ + @Override + public String toString() { + return String.format(mIssue.getFormat(), mParams); + } + } + + /** + * Wrapped around {@code byte[]} which ensures that {@code equals} and {@code hashCode} operate + * on the contents of the arrays rather than on references. + */ + private static class ByteArray { + private final byte[] mArray; + private final int mHashCode; + + private ByteArray(byte[] arr) { + mArray = arr; + mHashCode = Arrays.hashCode(mArray); + } + + @Override + public int hashCode() { + return mHashCode; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof ByteArray)) { + return false; + } + ByteArray other = (ByteArray) obj; + if (hashCode() != other.hashCode()) { + return false; + } + if (!Arrays.equals(mArray, other.mArray)) { + return false; + } + return true; + } + } + + /** + * Builder of {@link ApkVerifier} instances. + * + * <p>The resulting verifier by default checks whether the APK will verify on all platform + * versions supported by the APK, as specified by {@code android:minSdkVersion} attributes in + * the APK's {@code AndroidManifest.xml}. The range of platform versions can be customized using + * {@link #setMinCheckedPlatformVersion(int)} and {@link #setMaxCheckedPlatformVersion(int)}. + */ + public static class Builder { + private final File mApkFile; + private final DataSource mApkDataSource; + private File mV4SignatureFile; + + private Integer mMinSdkVersion; + private int mMaxSdkVersion = Integer.MAX_VALUE; + + /** + * Constructs a new {@code Builder} for verifying the provided APK file. + */ + public Builder(File apk) { + if (apk == null) { + throw new NullPointerException("apk == null"); + } + mApkFile = apk; + mApkDataSource = null; + } + + /** + * Constructs a new {@code Builder} for verifying the provided APK. + */ + public Builder(DataSource apk) { + if (apk == null) { + throw new NullPointerException("apk == null"); + } + mApkDataSource = apk; + mApkFile = null; + } + + /** + * Sets the oldest Android platform version for which the APK is verified. APK verification + * will confirm that the APK is expected to install successfully on all known Android + * platforms starting from the platform version with the provided API Level. The upper end + * of the platform versions range can be modified via + * {@link #setMaxCheckedPlatformVersion(int)}. + * + * <p>This method is useful for overriding the default behavior which checks that the APK + * will verify on all platform versions supported by the APK, as specified by + * {@code android:minSdkVersion} attributes in the APK's {@code AndroidManifest.xml}. + * + * @param minSdkVersion API Level of the oldest platform for which to verify the APK + * @see #setMinCheckedPlatformVersion(int) + */ + public Builder setMinCheckedPlatformVersion(int minSdkVersion) { + mMinSdkVersion = minSdkVersion; + return this; + } + + /** + * Sets the newest Android platform version for which the APK is verified. APK verification + * will confirm that the APK is expected to install successfully on all platform versions + * supported by the APK up until and including the provided version. The lower end + * of the platform versions range can be modified via + * {@link #setMinCheckedPlatformVersion(int)}. + * + * @param maxSdkVersion API Level of the newest platform for which to verify the APK + * @see #setMinCheckedPlatformVersion(int) + */ + public Builder setMaxCheckedPlatformVersion(int maxSdkVersion) { + mMaxSdkVersion = maxSdkVersion; + return this; + } + + public Builder setV4SignatureFile(File v4SignatureFile) { + mV4SignatureFile = v4SignatureFile; + return this; + } + + /** + * Returns an {@link ApkVerifier} initialized according to the configuration of this + * builder. + */ + public ApkVerifier build() { + return new ApkVerifier( + mApkFile, + mApkDataSource, + mV4SignatureFile, + mMinSdkVersion, + mMaxSdkVersion); + } + } + + /** + * Adapter for converting base {@link ApkVerificationIssue} instances to their {@link + * IssueWithParams} equivalent. + */ + public static class ApkVerificationIssueAdapter { + private ApkVerificationIssueAdapter() { + } + + // This field is visible for testing + static final Map<Integer, Issue> sVerificationIssueIdToIssue = new HashMap<>(); + + static { + sVerificationIssueIdToIssue.put(ApkVerificationIssue.V2_SIG_MALFORMED_SIGNERS, + Issue.V2_SIG_MALFORMED_SIGNERS); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.V2_SIG_NO_SIGNERS, + Issue.V2_SIG_NO_SIGNERS); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.V2_SIG_MALFORMED_SIGNER, + Issue.V2_SIG_MALFORMED_SIGNER); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.V2_SIG_MALFORMED_SIGNATURE, + Issue.V2_SIG_MALFORMED_SIGNATURE); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.V2_SIG_NO_SIGNATURES, + Issue.V2_SIG_NO_SIGNATURES); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.V2_SIG_MALFORMED_CERTIFICATE, + Issue.V2_SIG_MALFORMED_CERTIFICATE); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.V2_SIG_NO_CERTIFICATES, + Issue.V2_SIG_NO_CERTIFICATES); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.V2_SIG_MALFORMED_DIGEST, + Issue.V2_SIG_MALFORMED_DIGEST); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.V3_SIG_MALFORMED_SIGNERS, + Issue.V3_SIG_MALFORMED_SIGNERS); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.V3_SIG_NO_SIGNERS, + Issue.V3_SIG_NO_SIGNERS); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.V3_SIG_MALFORMED_SIGNER, + Issue.V3_SIG_MALFORMED_SIGNER); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.V3_SIG_MALFORMED_SIGNATURE, + Issue.V3_SIG_MALFORMED_SIGNATURE); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.V3_SIG_NO_SIGNATURES, + Issue.V3_SIG_NO_SIGNATURES); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.V3_SIG_MALFORMED_CERTIFICATE, + Issue.V3_SIG_MALFORMED_CERTIFICATE); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.V3_SIG_NO_CERTIFICATES, + Issue.V3_SIG_NO_CERTIFICATES); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.V3_SIG_MALFORMED_DIGEST, + Issue.V3_SIG_MALFORMED_DIGEST); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_NO_SIGNATURE, + Issue.SOURCE_STAMP_NO_SIGNATURE); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_MALFORMED_CERTIFICATE, + Issue.SOURCE_STAMP_MALFORMED_CERTIFICATE); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_UNKNOWN_SIG_ALGORITHM, + Issue.SOURCE_STAMP_UNKNOWN_SIG_ALGORITHM); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_MALFORMED_SIGNATURE, + Issue.SOURCE_STAMP_MALFORMED_SIGNATURE); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_DID_NOT_VERIFY, + Issue.SOURCE_STAMP_DID_NOT_VERIFY); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_VERIFY_EXCEPTION, + Issue.SOURCE_STAMP_VERIFY_EXCEPTION); + sVerificationIssueIdToIssue.put( + ApkVerificationIssue.SOURCE_STAMP_EXPECTED_DIGEST_MISMATCH, + Issue.SOURCE_STAMP_EXPECTED_DIGEST_MISMATCH); + sVerificationIssueIdToIssue.put( + ApkVerificationIssue.SOURCE_STAMP_SIGNATURE_BLOCK_WITHOUT_CERT_DIGEST, + Issue.SOURCE_STAMP_SIGNATURE_BLOCK_WITHOUT_CERT_DIGEST); + sVerificationIssueIdToIssue.put( + ApkVerificationIssue.SOURCE_STAMP_CERT_DIGEST_AND_SIG_BLOCK_MISSING, + Issue.SOURCE_STAMP_CERT_DIGEST_AND_SIG_BLOCK_MISSING); + sVerificationIssueIdToIssue.put( + ApkVerificationIssue.SOURCE_STAMP_NO_SUPPORTED_SIGNATURE, + Issue.SOURCE_STAMP_NO_SUPPORTED_SIGNATURE); + sVerificationIssueIdToIssue.put( + ApkVerificationIssue + .SOURCE_STAMP_CERTIFICATE_MISMATCH_BETWEEN_SIGNATURE_BLOCK_AND_APK, + Issue.SOURCE_STAMP_CERTIFICATE_MISMATCH_BETWEEN_SIGNATURE_BLOCK_AND_APK); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.MALFORMED_APK, + Issue.MALFORMED_APK); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.UNEXPECTED_EXCEPTION, + Issue.UNEXPECTED_EXCEPTION); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_SIG_MISSING, + Issue.SOURCE_STAMP_SIG_MISSING); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_MALFORMED_ATTRIBUTE, + Issue.SOURCE_STAMP_MALFORMED_ATTRIBUTE); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_UNKNOWN_ATTRIBUTE, + Issue.SOURCE_STAMP_UNKNOWN_ATTRIBUTE); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_MALFORMED_LINEAGE, + Issue.SOURCE_STAMP_MALFORMED_LINEAGE); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_POR_CERT_MISMATCH, + Issue.SOURCE_STAMP_POR_CERT_MISMATCH); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_POR_DID_NOT_VERIFY, + Issue.SOURCE_STAMP_POR_DID_NOT_VERIFY); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.JAR_SIG_NO_SIGNATURES, + Issue.JAR_SIG_NO_SIGNATURES); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.JAR_SIG_PARSE_EXCEPTION, + Issue.JAR_SIG_PARSE_EXCEPTION); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_INVALID_TIMESTAMP, + Issue.SOURCE_STAMP_INVALID_TIMESTAMP); + } + + /** + * Converts the provided {@code verificationIssues} to a {@code List} of corresponding + * {@link IssueWithParams} instances. + */ + public static List<IssueWithParams> getIssuesFromVerificationIssues( + List<? extends ApkVerificationIssue> verificationIssues) { + List<IssueWithParams> result = new ArrayList<>(verificationIssues.size()); + for (ApkVerificationIssue issue : verificationIssues) { + if (issue instanceof IssueWithParams) { + result.add((IssueWithParams) issue); + } else { + result.add( + new IssueWithParams(sVerificationIssueIdToIssue.get(issue.getIssueId()), + issue.getParams())); + } + } + return result; + } + } +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/Constants.java b/platform/android/java/editor/src/main/java/com/android/apksig/Constants.java new file mode 100644 index 0000000000..dd33028cd5 --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/Constants.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig; + +import com.android.apksig.internal.apk.stamp.SourceStampConstants; +import com.android.apksig.internal.apk.v1.V1SchemeConstants; +import com.android.apksig.internal.apk.v2.V2SchemeConstants; +import com.android.apksig.internal.apk.v3.V3SchemeConstants; + +/** + * Exports internally defined constants to allow clients to reference these values without relying + * on internal code. + */ +public class Constants { + private Constants() {} + + public static final int VERSION_SOURCE_STAMP = 0; + public static final int VERSION_JAR_SIGNATURE_SCHEME = 1; + public static final int VERSION_APK_SIGNATURE_SCHEME_V2 = 2; + public static final int VERSION_APK_SIGNATURE_SCHEME_V3 = 3; + public static final int VERSION_APK_SIGNATURE_SCHEME_V31 = 31; + public static final int VERSION_APK_SIGNATURE_SCHEME_V4 = 4; + + /** + * The maximum number of signers supported by the v1 and v2 APK Signature Schemes. + */ + public static final int MAX_APK_SIGNERS = 10; + + /** + * The default page alignment for native library files in bytes. + */ + public static final short LIBRARY_PAGE_ALIGNMENT_BYTES = 16384; + + public static final String MANIFEST_ENTRY_NAME = V1SchemeConstants.MANIFEST_ENTRY_NAME; + + public static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID = + V2SchemeConstants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID; + + public static final int APK_SIGNATURE_SCHEME_V3_BLOCK_ID = + V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID; + public static final int APK_SIGNATURE_SCHEME_V31_BLOCK_ID = + V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID; + public static final int PROOF_OF_ROTATION_ATTR_ID = V3SchemeConstants.PROOF_OF_ROTATION_ATTR_ID; + + public static final int V1_SOURCE_STAMP_BLOCK_ID = + SourceStampConstants.V1_SOURCE_STAMP_BLOCK_ID; + public static final int V2_SOURCE_STAMP_BLOCK_ID = + SourceStampConstants.V2_SOURCE_STAMP_BLOCK_ID; + + public static final String OID_RSA_ENCRYPTION = "1.2.840.113549.1.1.1"; +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/DefaultApkSignerEngine.java b/platform/android/java/editor/src/main/java/com/android/apksig/DefaultApkSignerEngine.java new file mode 100644 index 0000000000..957f48ad43 --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/DefaultApkSignerEngine.java @@ -0,0 +1,2241 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig; + +import static com.android.apksig.apk.ApkUtils.SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME; +import static com.android.apksig.apk.ApkUtils.computeSha256DigestBytes; +import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERITY_PADDING_BLOCK_ID; +import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2; +import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3; +import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_JAR_SIGNATURE_SCHEME; +import static com.android.apksig.internal.apk.v3.V3SchemeConstants.MIN_SDK_WITH_V31_SUPPORT; +import static com.android.apksig.internal.apk.v3.V3SchemeConstants.MIN_SDK_WITH_V3_SUPPORT; + +import com.android.apksig.apk.ApkFormatException; +import com.android.apksig.apk.ApkUtils; +import com.android.apksig.internal.apk.ApkSigningBlockUtils; +import com.android.apksig.internal.apk.ContentDigestAlgorithm; +import com.android.apksig.internal.apk.SignatureAlgorithm; +import com.android.apksig.internal.apk.stamp.V2SourceStampSigner; +import com.android.apksig.internal.apk.v1.DigestAlgorithm; +import com.android.apksig.internal.apk.v1.V1SchemeConstants; +import com.android.apksig.internal.apk.v1.V1SchemeSigner; +import com.android.apksig.internal.apk.v1.V1SchemeVerifier; +import com.android.apksig.internal.apk.v2.V2SchemeSigner; +import com.android.apksig.internal.apk.v3.V3SchemeConstants; +import com.android.apksig.internal.apk.v3.V3SchemeSigner; +import com.android.apksig.internal.apk.v4.V4SchemeSigner; +import com.android.apksig.internal.apk.v4.V4Signature; +import com.android.apksig.internal.jar.ManifestParser; +import com.android.apksig.internal.util.AndroidSdkVersion; +import com.android.apksig.internal.util.Pair; +import com.android.apksig.internal.util.TeeDataSink; +import com.android.apksig.util.DataSink; +import com.android.apksig.util.DataSinks; +import com.android.apksig.util.DataSource; +import com.android.apksig.util.RunnablesExecutor; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.security.InvalidKeyException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SignatureException; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Default implementation of {@link ApkSignerEngine}. + * + * <p>Use {@link Builder} to obtain instances of this engine. + */ +public class DefaultApkSignerEngine implements ApkSignerEngine { + + // IMPLEMENTATION NOTE: This engine generates a signed APK as follows: + // 1. The engine asks its client to output input JAR entries which are not part of JAR + // signature. + // 2. If JAR signing (v1 signing) is enabled, the engine inspects the output JAR entries to + // compute their digests, to be placed into output META-INF/MANIFEST.MF. It also inspects + // the contents of input and output META-INF/MANIFEST.MF to borrow the main section of the + // file. It does not care about individual (i.e., JAR entry-specific) sections. It then + // emits the v1 signature (a set of JAR entries) and asks the client to output them. + // 3. If APK Signature Scheme v2 (v2 signing) is enabled, the engine emits an APK Signing Block + // from outputZipSections() and asks its client to insert this block into the output. + // 4. If APK Signature Scheme v3 (v3 signing) is enabled, the engine includes it in the APK + // Signing BLock output from outputZipSections() and asks its client to insert this block + // into the output. If both v2 and v3 signing is enabled, they are both added to the APK + // Signing Block before asking the client to insert it into the output. + + private final boolean mV1SigningEnabled; + private final boolean mV2SigningEnabled; + private final boolean mV3SigningEnabled; + private final boolean mVerityEnabled; + private final boolean mDebuggableApkPermitted; + private final boolean mOtherSignersSignaturesPreserved; + private final String mCreatedBy; + private final List<SignerConfig> mSignerConfigs; + private final List<SignerConfig> mTargetedSignerConfigs; + private final SignerConfig mSourceStampSignerConfig; + private final SigningCertificateLineage mSourceStampSigningCertificateLineage; + private final boolean mSourceStampTimestampEnabled; + private final int mMinSdkVersion; + private final SigningCertificateLineage mSigningCertificateLineage; + + private List<byte[]> mPreservedV2Signers = Collections.emptyList(); + private List<Pair<byte[], Integer>> mPreservedSignatureBlocks = Collections.emptyList(); + + private List<V1SchemeSigner.SignerConfig> mV1SignerConfigs = Collections.emptyList(); + private DigestAlgorithm mV1ContentDigestAlgorithm; + + private boolean mClosed; + + private boolean mV1SignaturePending; + + /** Names of JAR entries which this engine is expected to output as part of v1 signing. */ + private Set<String> mSignatureExpectedOutputJarEntryNames = Collections.emptySet(); + + /** Requests for digests of output JAR entries. */ + private final Map<String, GetJarEntryDataDigestRequest> mOutputJarEntryDigestRequests = + new HashMap<>(); + + /** Digests of output JAR entries. */ + private final Map<String, byte[]> mOutputJarEntryDigests = new HashMap<>(); + + /** Data of JAR entries emitted by this engine as v1 signature. */ + private final Map<String, byte[]> mEmittedSignatureJarEntryData = new HashMap<>(); + + /** Requests for data of output JAR entries which comprise the v1 signature. */ + private final Map<String, GetJarEntryDataRequest> mOutputSignatureJarEntryDataRequests = + new HashMap<>(); + /** + * Request to obtain the data of MANIFEST.MF or {@code null} if the request hasn't been issued. + */ + private GetJarEntryDataRequest mInputJarManifestEntryDataRequest; + + /** + * Request to obtain the data of AndroidManifest.xml or {@code null} if the request hasn't been + * issued. + */ + private GetJarEntryDataRequest mOutputAndroidManifestEntryDataRequest; + + /** + * Whether the package being signed is marked as {@code android:debuggable} or {@code null} if + * this is not yet known. + */ + private Boolean mDebuggable; + + /** + * Request to output the emitted v1 signature or {@code null} if the request hasn't been issued. + */ + private OutputJarSignatureRequestImpl mAddV1SignatureRequest; + + private boolean mV2SignaturePending; + private boolean mV3SignaturePending; + + /** + * Request to output the emitted v2 and/or v3 signature(s) {@code null} if the request hasn't + * been issued. + */ + private OutputApkSigningBlockRequestImpl mAddSigningBlockRequest; + + private RunnablesExecutor mExecutor = RunnablesExecutor.MULTI_THREADED; + + /** + * A Set of block IDs to be discarded when requesting to preserve the original signatures. + */ + private static final Set<Integer> DISCARDED_SIGNATURE_BLOCK_IDS; + static { + DISCARDED_SIGNATURE_BLOCK_IDS = new HashSet<>(3); + // The verity padding block is recomputed on an + // ApkSigningBlockUtils.ANDROID_COMMON_PAGE_ALIGNMENT_BYTES boundary. + DISCARDED_SIGNATURE_BLOCK_IDS.add(VERITY_PADDING_BLOCK_ID); + // The source stamp block is not currently preserved; appending a new signature scheme + // block will invalidate the previous source stamp. + DISCARDED_SIGNATURE_BLOCK_IDS.add(Constants.V1_SOURCE_STAMP_BLOCK_ID); + DISCARDED_SIGNATURE_BLOCK_IDS.add(Constants.V2_SOURCE_STAMP_BLOCK_ID); + } + + private DefaultApkSignerEngine( + List<SignerConfig> signerConfigs, + List<SignerConfig> targetedSignerConfigs, + SignerConfig sourceStampSignerConfig, + SigningCertificateLineage sourceStampSigningCertificateLineage, + boolean sourceStampTimestampEnabled, + int minSdkVersion, + boolean v1SigningEnabled, + boolean v2SigningEnabled, + boolean v3SigningEnabled, + boolean verityEnabled, + boolean debuggableApkPermitted, + boolean otherSignersSignaturesPreserved, + String createdBy, + SigningCertificateLineage signingCertificateLineage) + throws InvalidKeyException { + if (signerConfigs.isEmpty() && targetedSignerConfigs.isEmpty()) { + throw new IllegalArgumentException("At least one signer config must be provided"); + } + + mV1SigningEnabled = v1SigningEnabled; + mV2SigningEnabled = v2SigningEnabled; + mV3SigningEnabled = v3SigningEnabled; + mVerityEnabled = verityEnabled; + mV1SignaturePending = v1SigningEnabled; + mV2SignaturePending = v2SigningEnabled; + mV3SignaturePending = v3SigningEnabled; + mDebuggableApkPermitted = debuggableApkPermitted; + mOtherSignersSignaturesPreserved = otherSignersSignaturesPreserved; + mCreatedBy = createdBy; + mSignerConfigs = signerConfigs; + mTargetedSignerConfigs = targetedSignerConfigs; + mSourceStampSignerConfig = sourceStampSignerConfig; + mSourceStampSigningCertificateLineage = sourceStampSigningCertificateLineage; + mSourceStampTimestampEnabled = sourceStampTimestampEnabled; + mMinSdkVersion = minSdkVersion; + mSigningCertificateLineage = signingCertificateLineage; + + if (v1SigningEnabled) { + if (v3SigningEnabled) { + + // v3 signing only supports single signers, of which the oldest (first) will be the + // one to use for v1 and v2 signing + SignerConfig oldestConfig = !signerConfigs.isEmpty() ? signerConfigs.get(0) + : targetedSignerConfigs.get(0); + + // in the event of signing certificate changes, make sure we have the oldest in the + // signing history to sign with v1 + if (signingCertificateLineage != null) { + SigningCertificateLineage subLineage = + signingCertificateLineage.getSubLineage( + oldestConfig.mCertificates.get(0)); + if (subLineage.size() != 1) { + throw new IllegalArgumentException( + "v1 signing enabled but the oldest signer in the" + + " SigningCertificateLineage is missing. Please provide the" + + " oldest signer to enable v1 signing"); + } + } + createV1SignerConfigs(Collections.singletonList(oldestConfig), minSdkVersion); + } else { + createV1SignerConfigs(signerConfigs, minSdkVersion); + } + } + } + + private void createV1SignerConfigs(List<SignerConfig> signerConfigs, int minSdkVersion) + throws InvalidKeyException { + mV1SignerConfigs = new ArrayList<>(signerConfigs.size()); + Map<String, Integer> v1SignerNameToSignerIndex = new HashMap<>(signerConfigs.size()); + DigestAlgorithm v1ContentDigestAlgorithm = null; + for (int i = 0; i < signerConfigs.size(); i++) { + SignerConfig signerConfig = signerConfigs.get(i); + List<X509Certificate> certificates = signerConfig.getCertificates(); + PublicKey publicKey = certificates.get(0).getPublicKey(); + + String v1SignerName = V1SchemeSigner.getSafeSignerName(signerConfig.getName()); + // Check whether the signer's name is unique among all v1 signers + Integer indexOfOtherSignerWithSameName = v1SignerNameToSignerIndex.put(v1SignerName, i); + if (indexOfOtherSignerWithSameName != null) { + throw new IllegalArgumentException( + "Signers #" + + (indexOfOtherSignerWithSameName + 1) + + " and #" + + (i + 1) + + " have the same name: " + + v1SignerName + + ". v1 signer names must be unique"); + } + + DigestAlgorithm v1SignatureDigestAlgorithm = + V1SchemeSigner.getSuggestedSignatureDigestAlgorithm(publicKey, minSdkVersion); + V1SchemeSigner.SignerConfig v1SignerConfig = new V1SchemeSigner.SignerConfig(); + v1SignerConfig.name = v1SignerName; + v1SignerConfig.privateKey = signerConfig.getPrivateKey(); + v1SignerConfig.certificates = certificates; + v1SignerConfig.signatureDigestAlgorithm = v1SignatureDigestAlgorithm; + v1SignerConfig.deterministicDsaSigning = signerConfig.getDeterministicDsaSigning(); + // For digesting contents of APK entries and of MANIFEST.MF, pick the algorithm + // of comparable strength to the digest algorithm used for computing the signature. + // When there are multiple signers, pick the strongest digest algorithm out of their + // signature digest algorithms. This avoids reducing the digest strength used by any + // of the signers to protect APK contents. + if (v1ContentDigestAlgorithm == null) { + v1ContentDigestAlgorithm = v1SignatureDigestAlgorithm; + } else { + if (DigestAlgorithm.BY_STRENGTH_COMPARATOR.compare( + v1SignatureDigestAlgorithm, v1ContentDigestAlgorithm) + > 0) { + v1ContentDigestAlgorithm = v1SignatureDigestAlgorithm; + } + } + mV1SignerConfigs.add(v1SignerConfig); + } + mV1ContentDigestAlgorithm = v1ContentDigestAlgorithm; + mSignatureExpectedOutputJarEntryNames = + V1SchemeSigner.getOutputEntryNames(mV1SignerConfigs); + } + + private List<ApkSigningBlockUtils.SignerConfig> createV2SignerConfigs( + boolean apkSigningBlockPaddingSupported) throws InvalidKeyException { + if (mV3SigningEnabled) { + + // v3 signing only supports single signers, of which the oldest (first) will be the one + // to use for v1 and v2 signing + List<ApkSigningBlockUtils.SignerConfig> signerConfig = new ArrayList<>(); + + SignerConfig oldestConfig = !mSignerConfigs.isEmpty() ? mSignerConfigs.get(0) + : mTargetedSignerConfigs.get(0); + + // first make sure that if we have signing certificate history that the oldest signer + // corresponds to the oldest ancestor + if (mSigningCertificateLineage != null) { + SigningCertificateLineage subLineage = + mSigningCertificateLineage.getSubLineage(oldestConfig.mCertificates.get(0)); + if (subLineage.size() != 1) { + throw new IllegalArgumentException( + "v2 signing enabled but the oldest signer in" + + " the SigningCertificateLineage is missing. Please provide" + + " the oldest signer to enable v2 signing."); + } + } + signerConfig.add( + createSigningBlockSignerConfig( + oldestConfig, + apkSigningBlockPaddingSupported, + ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2)); + return signerConfig; + } else { + return createSigningBlockSignerConfigs( + apkSigningBlockPaddingSupported, + ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2); + } + } + + private List<ApkSigningBlockUtils.SignerConfig> processV3Configs( + List<ApkSigningBlockUtils.SignerConfig> rawConfigs) throws InvalidKeyException { + // If the caller only specified targeted signing configs, ensure those configs cover the + // full range for V3 support (or the APK's minSdkVersion if > P). + int minRequiredV3SdkVersion = Math.max(AndroidSdkVersion.P, mMinSdkVersion); + if (mSignerConfigs.isEmpty() && + mTargetedSignerConfigs.get(0).getMinSdkVersion() > minRequiredV3SdkVersion) { + throw new IllegalArgumentException( + "The provided targeted signer configs do not cover the SDK range for V3 " + + "support; either provide the original signer or ensure a signer " + + "targets SDK version " + minRequiredV3SdkVersion); + } + + List<ApkSigningBlockUtils.SignerConfig> processedConfigs = new ArrayList<>(); + + // we have our configs, now touch them up to appropriately cover all SDK levels since APK + // signature scheme v3 was introduced + int currentMinSdk = Integer.MAX_VALUE; + for (int i = rawConfigs.size() - 1; i >= 0; i--) { + ApkSigningBlockUtils.SignerConfig config = rawConfigs.get(i); + if (config.signatureAlgorithms == null) { + // no valid algorithm was found for this signer, and we haven't yet covered all + // platform versions, something's wrong + String keyAlgorithm = config.certificates.get(0).getPublicKey().getAlgorithm(); + throw new InvalidKeyException( + "Unsupported key algorithm " + + keyAlgorithm + + " is " + + "not supported for APK Signature Scheme v3 signing"); + } + if (i == rawConfigs.size() - 1) { + // first go through the loop, config should support all future platform versions. + // this assumes we don't deprecate support for signers in the future. If we do, + // this needs to change + config.maxSdkVersion = Integer.MAX_VALUE; + } else { + // If the previous signer was targeting a development release, then the current + // signer's maxSdkVersion should overlap with the previous signer's minSdkVersion + // to ensure the current signer applies to the production release. + ApkSigningBlockUtils.SignerConfig prevSigner = processedConfigs.get( + processedConfigs.size() - 1); + if (prevSigner.signerTargetsDevRelease) { + config.maxSdkVersion = prevSigner.minSdkVersion; + } else { + config.maxSdkVersion = currentMinSdk - 1; + } + } + if (config.minSdkVersion == V3SchemeConstants.DEV_RELEASE) { + // If the current signer is targeting the current development release, then set + // the signer's minSdkVersion to the last production release and the flag indicating + // this signer is targeting a dev release. + config.minSdkVersion = V3SchemeConstants.PROD_RELEASE; + config.signerTargetsDevRelease = true; + } else if (config.minSdkVersion == 0) { + config.minSdkVersion = getMinSdkFromV3SignatureAlgorithms( + config.signatureAlgorithms); + } + // Truncate the lineage to the current signer if it is not the latest signer. + X509Certificate signerCert = config.certificates.get(0); + if (config.signingCertificateLineage != null + && !config.signingCertificateLineage.isCertificateLatestInLineage(signerCert)) { + config.signingCertificateLineage = config.signingCertificateLineage.getSubLineage( + signerCert); + } + // we know that this config will be used, so add it to our result, order doesn't matter + // at this point + processedConfigs.add(config); + currentMinSdk = config.minSdkVersion; + if (config.signerTargetsDevRelease ? currentMinSdk < minRequiredV3SdkVersion + : currentMinSdk <= minRequiredV3SdkVersion) { + // this satisfies all we need, stop here + break; + } + } + if (currentMinSdk > AndroidSdkVersion.P && currentMinSdk > mMinSdkVersion) { + // we can't cover all desired SDK versions, abort + throw new InvalidKeyException( + "Provided key algorithms not supported on all desired " + + "Android SDK versions"); + } + + return processedConfigs; + } + + private List<ApkSigningBlockUtils.SignerConfig> createV3SignerConfigs( + boolean apkSigningBlockPaddingSupported) throws InvalidKeyException { + return processV3Configs(createSigningBlockSignerConfigs(apkSigningBlockPaddingSupported, + ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3)); + } + + private List<ApkSigningBlockUtils.SignerConfig> processV31SignerConfigs( + List<ApkSigningBlockUtils.SignerConfig> v3SignerConfigs) { + // The V3.1 signature scheme supports SDK targeted signing config, but this scheme should + // only be used when a separate signing config exists for the V3.0 block. + if (v3SignerConfigs.size() == 1) { + return null; + } + + // When there are multiple signing configs, the signer with the minimum SDK version should + // be used for the V3.0 block, and all other signers should be used for the V3.1 block. + int signerMinSdkVersion = v3SignerConfigs.stream().mapToInt( + signer -> signer.minSdkVersion).min().orElse(AndroidSdkVersion.P); + List<ApkSigningBlockUtils.SignerConfig> v31SignerConfigs = new ArrayList<>(); + Iterator<ApkSigningBlockUtils.SignerConfig> v3SignerIterator = v3SignerConfigs.iterator(); + while (v3SignerIterator.hasNext()) { + ApkSigningBlockUtils.SignerConfig signerConfig = v3SignerIterator.next(); + // If the signer config's minSdkVersion supports V3.1 and is not the min signer in the + // list, then add it to the V3.1 signer configs and remove it from the V3.0 list. If + // the signer is targeting the minSdkVersion as a development release, then it should + // be included in V3.1 to allow the V3.0 block to target the production release of the + // same SDK version. + if (signerConfig.minSdkVersion >= MIN_SDK_WITH_V31_SUPPORT + && (signerConfig.minSdkVersion > signerMinSdkVersion + || (signerConfig.minSdkVersion >= signerMinSdkVersion + && signerConfig.signerTargetsDevRelease))) { + v31SignerConfigs.add(signerConfig); + v3SignerIterator.remove(); + } + } + return v31SignerConfigs; + } + + private V4SchemeSigner.SignerConfig createV4SignerConfig() throws InvalidKeyException { + List<ApkSigningBlockUtils.SignerConfig> v4Configs = createSigningBlockSignerConfigs(true, + ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V4); + if (v4Configs.size() != 1) { + // V4 uses signer config to connect back to v3. Use the same filtering logic. + v4Configs = processV3Configs(v4Configs); + } + List<ApkSigningBlockUtils.SignerConfig> v41configs = processV31SignerConfigs(v4Configs); + return new V4SchemeSigner.SignerConfig(v4Configs, v41configs); + } + + private ApkSigningBlockUtils.SignerConfig createSourceStampSignerConfig() + throws InvalidKeyException { + ApkSigningBlockUtils.SignerConfig config = createSigningBlockSignerConfig( + mSourceStampSignerConfig, + /* apkSigningBlockPaddingSupported= */ false, + ApkSigningBlockUtils.VERSION_SOURCE_STAMP); + if (mSourceStampSigningCertificateLineage != null) { + config.signingCertificateLineage = mSourceStampSigningCertificateLineage.getSubLineage( + config.certificates.get(0)); + } + return config; + } + + private int getMinSdkFromV3SignatureAlgorithms(List<SignatureAlgorithm> algorithms) { + int min = Integer.MAX_VALUE; + for (SignatureAlgorithm algorithm : algorithms) { + int current = algorithm.getMinSdkVersion(); + if (current < min) { + if (current <= mMinSdkVersion || current <= AndroidSdkVersion.P) { + // this algorithm satisfies all of our needs, no need to keep looking + return current; + } else { + min = current; + } + } + } + return min; + } + + private List<ApkSigningBlockUtils.SignerConfig> createSigningBlockSignerConfigs( + boolean apkSigningBlockPaddingSupported, int schemeId) throws InvalidKeyException { + List<ApkSigningBlockUtils.SignerConfig> signerConfigs = + new ArrayList<>(mSignerConfigs.size() + mTargetedSignerConfigs.size()); + for (int i = 0; i < mSignerConfigs.size(); i++) { + SignerConfig signerConfig = mSignerConfigs.get(i); + signerConfigs.add( + createSigningBlockSignerConfig( + signerConfig, apkSigningBlockPaddingSupported, schemeId)); + } + if (schemeId >= VERSION_APK_SIGNATURE_SCHEME_V3) { + for (int i = 0; i < mTargetedSignerConfigs.size(); i++) { + SignerConfig signerConfig = mTargetedSignerConfigs.get(i); + signerConfigs.add( + createSigningBlockSignerConfig( + signerConfig, apkSigningBlockPaddingSupported, schemeId)); + } + } + return signerConfigs; + } + + private ApkSigningBlockUtils.SignerConfig createSigningBlockSignerConfig( + SignerConfig signerConfig, boolean apkSigningBlockPaddingSupported, int schemeId) + throws InvalidKeyException { + List<X509Certificate> certificates = signerConfig.getCertificates(); + PublicKey publicKey = certificates.get(0).getPublicKey(); + + ApkSigningBlockUtils.SignerConfig newSignerConfig = new ApkSigningBlockUtils.SignerConfig(); + newSignerConfig.privateKey = signerConfig.getPrivateKey(); + newSignerConfig.certificates = certificates; + newSignerConfig.minSdkVersion = signerConfig.getMinSdkVersion(); + newSignerConfig.signerTargetsDevRelease = signerConfig.getSignerTargetsDevRelease(); + newSignerConfig.signingCertificateLineage = signerConfig.getSigningCertificateLineage(); + + switch (schemeId) { + case ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2: + newSignerConfig.signatureAlgorithms = + V2SchemeSigner.getSuggestedSignatureAlgorithms( + publicKey, + mMinSdkVersion, + apkSigningBlockPaddingSupported && mVerityEnabled, + signerConfig.getDeterministicDsaSigning()); + break; + case ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3: + try { + newSignerConfig.signatureAlgorithms = + V3SchemeSigner.getSuggestedSignatureAlgorithms( + publicKey, + mMinSdkVersion, + apkSigningBlockPaddingSupported && mVerityEnabled, + signerConfig.getDeterministicDsaSigning()); + } catch (InvalidKeyException e) { + + // It is possible for a signer used for v1/v2 signing to not be allowed for use + // with v3 signing. This is ok as long as there exists a more recent v3 signer + // that covers all supported platform versions. Populate signatureAlgorithm + // with null, it will be cleaned-up in a later step. + newSignerConfig.signatureAlgorithms = null; + } + break; + case ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V4: + try { + newSignerConfig.signatureAlgorithms = + V4SchemeSigner.getSuggestedSignatureAlgorithms( + publicKey, mMinSdkVersion, apkSigningBlockPaddingSupported, + signerConfig.getDeterministicDsaSigning()); + } catch (InvalidKeyException e) { + // V4 is an optional signing schema, ok to proceed without. + newSignerConfig.signatureAlgorithms = null; + } + break; + case ApkSigningBlockUtils.VERSION_SOURCE_STAMP: + newSignerConfig.signatureAlgorithms = + Collections.singletonList( + SignatureAlgorithm.RSA_PKCS1_V1_5_WITH_SHA256); + break; + default: + throw new IllegalArgumentException("Unknown APK Signature Scheme ID requested"); + } + return newSignerConfig; + } + + private boolean isDebuggable(String entryName) { + return mDebuggableApkPermitted + || !ApkUtils.ANDROID_MANIFEST_ZIP_ENTRY_NAME.equals(entryName); + } + + /** + * Initializes DefaultApkSignerEngine with the existing MANIFEST.MF. This reads existing digests + * from the MANIFEST.MF file (they are assumed correct) and stores them for the final signature + * without recalculation. This step has a significant performance benefit in case of incremental + * build. + * + * <p>This method extracts and stored computed digest for every entry that it would compute it + * for in the {@link #outputJarEntry(String)} method + * + * @param manifestBytes raw representation of MANIFEST.MF file + * @param entryNames a set of expected entries names + * @return set of entry names which were processed by the engine during the initialization, a + * subset of entryNames + */ + @Override + @SuppressWarnings("AndroidJdkLibsChecker") + public Set<String> initWith(byte[] manifestBytes, Set<String> entryNames) { + V1SchemeVerifier.Result result = new V1SchemeVerifier.Result(); + Pair<ManifestParser.Section, Map<String, ManifestParser.Section>> sections = + V1SchemeVerifier.parseManifest(manifestBytes, entryNames, result); + String alg = V1SchemeSigner.getJcaMessageDigestAlgorithm(mV1ContentDigestAlgorithm); + for (Map.Entry<String, ManifestParser.Section> entry : sections.getSecond().entrySet()) { + String entryName = entry.getKey(); + if (V1SchemeSigner.isJarEntryDigestNeededInManifest(entry.getKey()) + && isDebuggable(entryName)) { + + V1SchemeVerifier.NamedDigest extractedDigest = null; + Collection<V1SchemeVerifier.NamedDigest> digestsToVerify = + V1SchemeVerifier.getDigestsToVerify( + entry.getValue(), "-Digest", mMinSdkVersion, Integer.MAX_VALUE); + for (V1SchemeVerifier.NamedDigest digestToVerify : digestsToVerify) { + if (digestToVerify.jcaDigestAlgorithm.equals(alg)) { + extractedDigest = digestToVerify; + break; + } + } + if (extractedDigest != null) { + mOutputJarEntryDigests.put(entryName, extractedDigest.digest); + } + } + } + return mOutputJarEntryDigests.keySet(); + } + + @Override + public void setExecutor(RunnablesExecutor executor) { + mExecutor = executor; + } + + @Override + public void inputApkSigningBlock(DataSource apkSigningBlock) { + checkNotClosed(); + + if ((apkSigningBlock == null) || (apkSigningBlock.size() == 0)) { + return; + } + + if (mOtherSignersSignaturesPreserved) { + boolean schemeSignatureBlockPreserved = false; + mPreservedSignatureBlocks = new ArrayList<>(); + try { + List<Pair<byte[], Integer>> signatureBlocks = + ApkSigningBlockUtils.getApkSignatureBlocks(apkSigningBlock); + for (Pair<byte[], Integer> signatureBlock : signatureBlocks) { + if (signatureBlock.getSecond() == Constants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID) { + // If a V2 signature block is found and the engine is configured to use V2 + // then save any of the previous signers that are not part of the current + // signing request. + if (mV2SigningEnabled) { + List<Pair<List<X509Certificate>, byte[]>> v2Signers = + ApkSigningBlockUtils.getApkSignatureBlockSigners( + signatureBlock.getFirst()); + mPreservedV2Signers = new ArrayList<>(v2Signers.size()); + for (Pair<List<X509Certificate>, byte[]> v2Signer : v2Signers) { + if (!isConfiguredWithSigner(v2Signer.getFirst())) { + mPreservedV2Signers.add(v2Signer.getSecond()); + schemeSignatureBlockPreserved = true; + } + } + } else { + // else V2 signing is not enabled; save the entire signature block to be + // added to the final APK signing block. + mPreservedSignatureBlocks.add(signatureBlock); + schemeSignatureBlockPreserved = true; + } + } else if (signatureBlock.getSecond() + == Constants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID) { + // Preserving other signers in the presence of a V3 signature block is only + // supported if the engine is configured to resign the APK with the V3 + // signature scheme, and the V3 signer in the signature block is the same + // as the engine is configured to use. + if (!mV3SigningEnabled) { + throw new IllegalStateException( + "Preserving an existing V3 signature is not supported"); + } + List<Pair<List<X509Certificate>, byte[]>> v3Signers = + ApkSigningBlockUtils.getApkSignatureBlockSigners( + signatureBlock.getFirst()); + if (v3Signers.size() > 1) { + throw new IllegalArgumentException( + "The provided APK signing block contains " + v3Signers.size() + + " V3 signers; the V3 signature scheme only supports" + + " one signer"); + } + // If there is only a single V3 signer then ensure it is the signer + // configured to sign the APK. + if (v3Signers.size() == 1 + && !isConfiguredWithSigner(v3Signers.get(0).getFirst())) { + throw new IllegalStateException( + "The V3 signature scheme only supports one signer; a request " + + "was made to preserve the existing V3 signature, " + + "but the engine is configured to sign with a " + + "different signer"); + } + } else if (!DISCARDED_SIGNATURE_BLOCK_IDS.contains( + signatureBlock.getSecond())) { + mPreservedSignatureBlocks.add(signatureBlock); + } + } + } catch (ApkFormatException | CertificateException | IOException e) { + throw new IllegalArgumentException("Unable to parse the provided signing block", e); + } + // Signature scheme V3+ only support a single signer; if the engine is configured to + // sign with V3+ then ensure no scheme signature blocks have been preserved. + if (mV3SigningEnabled && schemeSignatureBlockPreserved) { + throw new IllegalStateException( + "Signature scheme V3+ only supports a single signer and cannot be " + + "appended to the existing signature scheme blocks"); + } + return; + } + } + + /** + * Returns whether the engine is configured to sign the APK with a signer using the specified + * {@code signerCerts}. + */ + private boolean isConfiguredWithSigner(List<X509Certificate> signerCerts) { + for (SignerConfig signerConfig : mSignerConfigs) { + if (signerCerts.containsAll(signerConfig.getCertificates())) { + return true; + } + } + return false; + } + + @Override + public InputJarEntryInstructions inputJarEntry(String entryName) { + checkNotClosed(); + + InputJarEntryInstructions.OutputPolicy outputPolicy = + getInputJarEntryOutputPolicy(entryName); + switch (outputPolicy) { + case SKIP: + return new InputJarEntryInstructions(InputJarEntryInstructions.OutputPolicy.SKIP); + case OUTPUT: + return new InputJarEntryInstructions(InputJarEntryInstructions.OutputPolicy.OUTPUT); + case OUTPUT_BY_ENGINE: + if (V1SchemeConstants.MANIFEST_ENTRY_NAME.equals(entryName)) { + // We copy the main section of the JAR manifest from input to output. Thus, this + // invalidates v1 signature and we need to see the entry's data. + mInputJarManifestEntryDataRequest = new GetJarEntryDataRequest(entryName); + return new InputJarEntryInstructions( + InputJarEntryInstructions.OutputPolicy.OUTPUT_BY_ENGINE, + mInputJarManifestEntryDataRequest); + } + return new InputJarEntryInstructions( + InputJarEntryInstructions.OutputPolicy.OUTPUT_BY_ENGINE); + default: + throw new RuntimeException("Unsupported output policy: " + outputPolicy); + } + } + + @Override + public InspectJarEntryRequest outputJarEntry(String entryName) { + checkNotClosed(); + invalidateV2Signature(); + + if (!isDebuggable(entryName)) { + forgetOutputApkDebuggableStatus(); + } + + if (!mV1SigningEnabled) { + // No need to inspect JAR entries when v1 signing is not enabled. + if (!isDebuggable(entryName)) { + // To reject debuggable APKs we need to inspect the APK's AndroidManifest.xml to + // check whether it declares that the APK is debuggable + mOutputAndroidManifestEntryDataRequest = new GetJarEntryDataRequest(entryName); + return mOutputAndroidManifestEntryDataRequest; + } + return null; + } + // v1 signing is enabled + + if (V1SchemeSigner.isJarEntryDigestNeededInManifest(entryName)) { + // This entry is covered by v1 signature. We thus need to inspect the entry's data to + // compute its digest(s) for v1 signature. + + // TODO: Handle the case where other signer's v1 signatures are present and need to be + // preserved. In that scenario we can't modify MANIFEST.MF and add/remove JAR entries + // covered by v1 signature. + invalidateV1Signature(); + GetJarEntryDataDigestRequest dataDigestRequest = + new GetJarEntryDataDigestRequest( + entryName, + V1SchemeSigner.getJcaMessageDigestAlgorithm(mV1ContentDigestAlgorithm)); + mOutputJarEntryDigestRequests.put(entryName, dataDigestRequest); + mOutputJarEntryDigests.remove(entryName); + + if ((!mDebuggableApkPermitted) + && (ApkUtils.ANDROID_MANIFEST_ZIP_ENTRY_NAME.equals(entryName))) { + // To reject debuggable APKs we need to inspect the APK's AndroidManifest.xml to + // check whether it declares that the APK is debuggable + mOutputAndroidManifestEntryDataRequest = new GetJarEntryDataRequest(entryName); + return new CompoundInspectJarEntryRequest( + entryName, mOutputAndroidManifestEntryDataRequest, dataDigestRequest); + } + + return dataDigestRequest; + } + + if (mSignatureExpectedOutputJarEntryNames.contains(entryName)) { + // This entry is part of v1 signature generated by this engine. We need to check whether + // the entry's data is as output by the engine. + invalidateV1Signature(); + GetJarEntryDataRequest dataRequest; + if (V1SchemeConstants.MANIFEST_ENTRY_NAME.equals(entryName)) { + dataRequest = new GetJarEntryDataRequest(entryName); + mInputJarManifestEntryDataRequest = dataRequest; + } else { + // If this entry is part of v1 signature which has been emitted by this engine, + // check whether the output entry's data matches what the engine emitted. + dataRequest = + (mEmittedSignatureJarEntryData.containsKey(entryName)) + ? new GetJarEntryDataRequest(entryName) + : null; + } + + if (dataRequest != null) { + mOutputSignatureJarEntryDataRequests.put(entryName, dataRequest); + } + return dataRequest; + } + + // This entry is not covered by v1 signature and isn't part of v1 signature. + return null; + } + + @Override + public InputJarEntryInstructions.OutputPolicy inputJarEntryRemoved(String entryName) { + checkNotClosed(); + return getInputJarEntryOutputPolicy(entryName); + } + + @Override + public void outputJarEntryRemoved(String entryName) { + checkNotClosed(); + invalidateV2Signature(); + if (!mV1SigningEnabled) { + return; + } + + if (V1SchemeSigner.isJarEntryDigestNeededInManifest(entryName)) { + // This entry is covered by v1 signature. + invalidateV1Signature(); + mOutputJarEntryDigests.remove(entryName); + mOutputJarEntryDigestRequests.remove(entryName); + mOutputSignatureJarEntryDataRequests.remove(entryName); + return; + } + + if (mSignatureExpectedOutputJarEntryNames.contains(entryName)) { + // This entry is part of the v1 signature generated by this engine. + invalidateV1Signature(); + return; + } + } + + @Override + public OutputJarSignatureRequest outputJarEntries() + throws ApkFormatException, InvalidKeyException, SignatureException, + NoSuchAlgorithmException { + checkNotClosed(); + + if (!mV1SignaturePending) { + return null; + } + + if ((mInputJarManifestEntryDataRequest != null) + && (!mInputJarManifestEntryDataRequest.isDone())) { + throw new IllegalStateException( + "Still waiting to inspect input APK's " + + mInputJarManifestEntryDataRequest.getEntryName()); + } + + for (GetJarEntryDataDigestRequest digestRequest : mOutputJarEntryDigestRequests.values()) { + String entryName = digestRequest.getEntryName(); + if (!digestRequest.isDone()) { + throw new IllegalStateException( + "Still waiting to inspect output APK's " + entryName); + } + mOutputJarEntryDigests.put(entryName, digestRequest.getDigest()); + } + if (isEligibleForSourceStamp()) { + MessageDigest messageDigest = + MessageDigest.getInstance( + V1SchemeSigner.getJcaMessageDigestAlgorithm(mV1ContentDigestAlgorithm)); + messageDigest.update(generateSourceStampCertificateDigest()); + mOutputJarEntryDigests.put( + SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME, messageDigest.digest()); + } + mOutputJarEntryDigestRequests.clear(); + + for (GetJarEntryDataRequest dataRequest : mOutputSignatureJarEntryDataRequests.values()) { + if (!dataRequest.isDone()) { + throw new IllegalStateException( + "Still waiting to inspect output APK's " + dataRequest.getEntryName()); + } + } + + List<Integer> apkSigningSchemeIds = new ArrayList<>(); + if (mV2SigningEnabled) { + apkSigningSchemeIds.add(ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2); + } + if (mV3SigningEnabled) { + apkSigningSchemeIds.add(ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3); + } + byte[] inputJarManifest = + (mInputJarManifestEntryDataRequest != null) + ? mInputJarManifestEntryDataRequest.getData() + : null; + if (isEligibleForSourceStamp()) { + inputJarManifest = + V1SchemeSigner.generateManifestFile( + mV1ContentDigestAlgorithm, + mOutputJarEntryDigests, + inputJarManifest) + .contents; + } + + // Check whether the most recently used signature (if present) is still fine. + checkOutputApkNotDebuggableIfDebuggableMustBeRejected(); + List<Pair<String, byte[]>> signatureZipEntries; + if ((mAddV1SignatureRequest == null) || (!mAddV1SignatureRequest.isDone())) { + try { + signatureZipEntries = + V1SchemeSigner.sign( + mV1SignerConfigs, + mV1ContentDigestAlgorithm, + mOutputJarEntryDigests, + apkSigningSchemeIds, + inputJarManifest, + mCreatedBy); + } catch (CertificateException e) { + throw new SignatureException("Failed to generate v1 signature", e); + } + } else { + V1SchemeSigner.OutputManifestFile newManifest = + V1SchemeSigner.generateManifestFile( + mV1ContentDigestAlgorithm, mOutputJarEntryDigests, inputJarManifest); + byte[] emittedSignatureManifest = + mEmittedSignatureJarEntryData.get(V1SchemeConstants.MANIFEST_ENTRY_NAME); + if (!Arrays.equals(newManifest.contents, emittedSignatureManifest)) { + // Emitted v1 signature is no longer valid. + try { + signatureZipEntries = + V1SchemeSigner.signManifest( + mV1SignerConfigs, + mV1ContentDigestAlgorithm, + apkSigningSchemeIds, + mCreatedBy, + newManifest); + } catch (CertificateException e) { + throw new SignatureException("Failed to generate v1 signature", e); + } + } else { + // Emitted v1 signature is still valid. Check whether the signature is there in the + // output. + signatureZipEntries = new ArrayList<>(); + for (Map.Entry<String, byte[]> expectedOutputEntry : + mEmittedSignatureJarEntryData.entrySet()) { + String entryName = expectedOutputEntry.getKey(); + byte[] expectedData = expectedOutputEntry.getValue(); + GetJarEntryDataRequest actualDataRequest = + mOutputSignatureJarEntryDataRequests.get(entryName); + if (actualDataRequest == null) { + // This signature entry hasn't been output. + signatureZipEntries.add(Pair.of(entryName, expectedData)); + continue; + } + byte[] actualData = actualDataRequest.getData(); + if (!Arrays.equals(expectedData, actualData)) { + signatureZipEntries.add(Pair.of(entryName, expectedData)); + } + } + if (signatureZipEntries.isEmpty()) { + // v1 signature in the output is valid + return null; + } + // v1 signature in the output is not valid. + } + } + + if (signatureZipEntries.isEmpty()) { + // v1 signature in the output is valid + mV1SignaturePending = false; + return null; + } + + List<OutputJarSignatureRequest.JarEntry> sigEntries = + new ArrayList<>(signatureZipEntries.size()); + for (Pair<String, byte[]> entry : signatureZipEntries) { + String entryName = entry.getFirst(); + byte[] entryData = entry.getSecond(); + sigEntries.add(new OutputJarSignatureRequest.JarEntry(entryName, entryData)); + mEmittedSignatureJarEntryData.put(entryName, entryData); + } + mAddV1SignatureRequest = new OutputJarSignatureRequestImpl(sigEntries); + return mAddV1SignatureRequest; + } + + @Deprecated + @Override + public OutputApkSigningBlockRequest outputZipSections( + DataSource zipEntries, DataSource zipCentralDirectory, DataSource zipEocd) + throws IOException, InvalidKeyException, SignatureException, NoSuchAlgorithmException { + return outputZipSectionsInternal(zipEntries, zipCentralDirectory, zipEocd, false); + } + + @Override + public OutputApkSigningBlockRequest2 outputZipSections2( + DataSource zipEntries, DataSource zipCentralDirectory, DataSource zipEocd) + throws IOException, InvalidKeyException, SignatureException, NoSuchAlgorithmException { + return outputZipSectionsInternal(zipEntries, zipCentralDirectory, zipEocd, true); + } + + private OutputApkSigningBlockRequestImpl outputZipSectionsInternal( + DataSource zipEntries, + DataSource zipCentralDirectory, + DataSource zipEocd, + boolean apkSigningBlockPaddingSupported) + throws IOException, InvalidKeyException, SignatureException, NoSuchAlgorithmException { + checkNotClosed(); + checkV1SigningDoneIfEnabled(); + if (!mV2SigningEnabled && !mV3SigningEnabled && !isEligibleForSourceStamp()) { + return null; + } + checkOutputApkNotDebuggableIfDebuggableMustBeRejected(); + + // adjust to proper padding + Pair<DataSource, Integer> paddingPair = + ApkSigningBlockUtils.generateApkSigningBlockPadding( + zipEntries, apkSigningBlockPaddingSupported); + DataSource beforeCentralDir = paddingPair.getFirst(); + int padSizeBeforeApkSigningBlock = paddingPair.getSecond(); + DataSource eocd = ApkSigningBlockUtils.copyWithModifiedCDOffset(beforeCentralDir, zipEocd); + + List<Pair<byte[], Integer>> signingSchemeBlocks = new ArrayList<>(); + ApkSigningBlockUtils.SigningSchemeBlockAndDigests v2SigningSchemeBlockAndDigests = null; + ApkSigningBlockUtils.SigningSchemeBlockAndDigests v3SigningSchemeBlockAndDigests = null; + // If the engine is configured to preserve previous signature blocks and any were found in + // the existing APK signing block then add them to the list to be used to generate the + // new APK signing block. + if (mOtherSignersSignaturesPreserved && mPreservedSignatureBlocks != null + && !mPreservedSignatureBlocks.isEmpty()) { + signingSchemeBlocks.addAll(mPreservedSignatureBlocks); + } + + // create APK Signature Scheme V2 Signature if requested + if (mV2SigningEnabled) { + invalidateV2Signature(); + List<ApkSigningBlockUtils.SignerConfig> v2SignerConfigs = + createV2SignerConfigs(apkSigningBlockPaddingSupported); + v2SigningSchemeBlockAndDigests = + V2SchemeSigner.generateApkSignatureSchemeV2Block( + mExecutor, + beforeCentralDir, + zipCentralDirectory, + eocd, + v2SignerConfigs, + mV3SigningEnabled, + mOtherSignersSignaturesPreserved ? mPreservedV2Signers : null); + signingSchemeBlocks.add(v2SigningSchemeBlockAndDigests.signingSchemeBlock); + } + if (mV3SigningEnabled) { + invalidateV3Signature(); + List<ApkSigningBlockUtils.SignerConfig> v3SignerConfigs = + createV3SignerConfigs(apkSigningBlockPaddingSupported); + List<ApkSigningBlockUtils.SignerConfig> v31SignerConfigs = processV31SignerConfigs( + v3SignerConfigs); + if (v31SignerConfigs != null && v31SignerConfigs.size() > 0) { + ApkSigningBlockUtils.SigningSchemeBlockAndDigests + v31SigningSchemeBlockAndDigests = + new V3SchemeSigner.Builder(beforeCentralDir, zipCentralDirectory, eocd, + v31SignerConfigs) + .setRunnablesExecutor(mExecutor) + .setBlockId(V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID) + .build() + .generateApkSignatureSchemeV3BlockAndDigests(); + signingSchemeBlocks.add(v31SigningSchemeBlockAndDigests.signingSchemeBlock); + } + V3SchemeSigner.Builder builder = new V3SchemeSigner.Builder(beforeCentralDir, + zipCentralDirectory, eocd, v3SignerConfigs) + .setRunnablesExecutor(mExecutor) + .setBlockId(V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID); + if (v31SignerConfigs != null && !v31SignerConfigs.isEmpty()) { + // The V3.1 stripping protection writes the minimum SDK version from the targeted + // signers as an additional attribute in the V3.0 signing block. + int minSdkVersionForV31 = v31SignerConfigs.stream().mapToInt( + signer -> signer.minSdkVersion).min().orElse(MIN_SDK_WITH_V31_SUPPORT); + builder.setMinSdkVersionForV31(minSdkVersionForV31); + } + v3SigningSchemeBlockAndDigests = + builder.build().generateApkSignatureSchemeV3BlockAndDigests(); + signingSchemeBlocks.add(v3SigningSchemeBlockAndDigests.signingSchemeBlock); + } + if (isEligibleForSourceStamp()) { + ApkSigningBlockUtils.SignerConfig sourceStampSignerConfig = + createSourceStampSignerConfig(); + Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeDigestInfos = + new HashMap<>(); + if (mV3SigningEnabled) { + signatureSchemeDigestInfos.put( + VERSION_APK_SIGNATURE_SCHEME_V3, v3SigningSchemeBlockAndDigests.digestInfo); + } + if (mV2SigningEnabled) { + signatureSchemeDigestInfos.put( + VERSION_APK_SIGNATURE_SCHEME_V2, v2SigningSchemeBlockAndDigests.digestInfo); + } + if (mV1SigningEnabled) { + Map<ContentDigestAlgorithm, byte[]> v1SigningSchemeDigests = new HashMap<>(); + try { + // Jar signing related variables must have been already populated at this point + // if V1 signing is enabled since it is happening before computations on the APK + // signing block (V2/V3/V4/SourceStamp signing). + byte[] inputJarManifest = + (mInputJarManifestEntryDataRequest != null) + ? mInputJarManifestEntryDataRequest.getData() + : null; + byte[] jarManifest = + V1SchemeSigner.generateManifestFile( + mV1ContentDigestAlgorithm, + mOutputJarEntryDigests, + inputJarManifest) + .contents; + // The digest of the jar manifest does not need to be computed in chunks due to + // the small size of the manifest. + v1SigningSchemeDigests.put( + ContentDigestAlgorithm.SHA256, computeSha256DigestBytes(jarManifest)); + } catch (ApkFormatException e) { + throw new RuntimeException("Failed to generate manifest file", e); + } + signatureSchemeDigestInfos.put( + VERSION_JAR_SIGNATURE_SCHEME, v1SigningSchemeDigests); + } + V2SourceStampSigner v2SourceStampSigner = + new V2SourceStampSigner.Builder(sourceStampSignerConfig, + signatureSchemeDigestInfos) + .setSourceStampTimestampEnabled(mSourceStampTimestampEnabled) + .build(); + signingSchemeBlocks.add(v2SourceStampSigner.generateSourceStampBlock()); + } + + // create APK Signing Block with v2 and/or v3 and/or SourceStamp blocks + byte[] apkSigningBlock = ApkSigningBlockUtils.generateApkSigningBlock(signingSchemeBlocks); + + mAddSigningBlockRequest = + new OutputApkSigningBlockRequestImpl(apkSigningBlock, padSizeBeforeApkSigningBlock); + return mAddSigningBlockRequest; + } + + @Override + public void outputDone() { + checkNotClosed(); + checkV1SigningDoneIfEnabled(); + checkSigningBlockDoneIfEnabled(); + } + + @Override + public void signV4(DataSource dataSource, File outputFile, boolean ignoreFailures) + throws SignatureException { + if (outputFile == null) { + if (ignoreFailures) { + return; + } + throw new SignatureException("Missing V4 output file."); + } + try { + V4SchemeSigner.SignerConfig v4SignerConfig = createV4SignerConfig(); + V4SchemeSigner.generateV4Signature(dataSource, v4SignerConfig, outputFile); + } catch (InvalidKeyException | IOException | NoSuchAlgorithmException e) { + if (ignoreFailures) { + return; + } + throw new SignatureException("V4 signing failed", e); + } + } + + /** For external use only to generate V4 & tree separately. */ + public byte[] produceV4Signature(DataSource dataSource, OutputStream sigOutput) + throws SignatureException { + if (sigOutput == null) { + throw new SignatureException("Missing V4 output streams."); + } + try { + V4SchemeSigner.SignerConfig v4SignerConfig = createV4SignerConfig(); + Pair<V4Signature, byte[]> pair = + V4SchemeSigner.generateV4Signature(dataSource, v4SignerConfig); + pair.getFirst().writeTo(sigOutput); + return pair.getSecond(); + } catch (InvalidKeyException | IOException | NoSuchAlgorithmException e) { + throw new SignatureException("V4 signing failed", e); + } + } + + @Override + public boolean isEligibleForSourceStamp() { + return mSourceStampSignerConfig != null + && (mV2SigningEnabled || mV3SigningEnabled || mV1SigningEnabled); + } + + @Override + public byte[] generateSourceStampCertificateDigest() throws SignatureException { + if (mSourceStampSignerConfig.getCertificates().isEmpty()) { + throw new SignatureException("No certificates configured for stamp"); + } + try { + return computeSha256DigestBytes( + mSourceStampSignerConfig.getCertificates().get(0).getEncoded()); + } catch (CertificateEncodingException e) { + throw new SignatureException("Failed to encode source stamp certificate", e); + } + } + + @Override + public void close() { + mClosed = true; + + mAddV1SignatureRequest = null; + mInputJarManifestEntryDataRequest = null; + mOutputAndroidManifestEntryDataRequest = null; + mDebuggable = null; + mOutputJarEntryDigestRequests.clear(); + mOutputJarEntryDigests.clear(); + mEmittedSignatureJarEntryData.clear(); + mOutputSignatureJarEntryDataRequests.clear(); + + mAddSigningBlockRequest = null; + } + + private void invalidateV1Signature() { + if (mV1SigningEnabled) { + mV1SignaturePending = true; + } + invalidateV2Signature(); + } + + private void invalidateV2Signature() { + if (mV2SigningEnabled) { + mV2SignaturePending = true; + mAddSigningBlockRequest = null; + } + } + + private void invalidateV3Signature() { + if (mV3SigningEnabled) { + mV3SignaturePending = true; + mAddSigningBlockRequest = null; + } + } + + private void checkNotClosed() { + if (mClosed) { + throw new IllegalStateException("Engine closed"); + } + } + + private void checkV1SigningDoneIfEnabled() { + if (!mV1SignaturePending) { + return; + } + + if (mAddV1SignatureRequest == null) { + throw new IllegalStateException( + "v1 signature (JAR signature) not yet generated. Skipped outputJarEntries()?"); + } + if (!mAddV1SignatureRequest.isDone()) { + throw new IllegalStateException( + "v1 signature (JAR signature) addition requested by outputJarEntries() hasn't" + + " been fulfilled"); + } + for (Map.Entry<String, byte[]> expectedOutputEntry : + mEmittedSignatureJarEntryData.entrySet()) { + String entryName = expectedOutputEntry.getKey(); + byte[] expectedData = expectedOutputEntry.getValue(); + GetJarEntryDataRequest actualDataRequest = + mOutputSignatureJarEntryDataRequests.get(entryName); + if (actualDataRequest == null) { + throw new IllegalStateException( + "APK entry " + + entryName + + " not yet output despite this having been" + + " requested"); + } else if (!actualDataRequest.isDone()) { + throw new IllegalStateException( + "Still waiting to inspect output APK's " + entryName); + } + byte[] actualData = actualDataRequest.getData(); + if (!Arrays.equals(expectedData, actualData)) { + throw new IllegalStateException( + "Output APK entry " + entryName + " data differs from what was requested"); + } + } + mV1SignaturePending = false; + } + + private void checkSigningBlockDoneIfEnabled() { + if (!mV2SignaturePending && !mV3SignaturePending) { + return; + } + if (mAddSigningBlockRequest == null) { + throw new IllegalStateException( + "Signed APK Signing BLock not yet generated. Skipped outputZipSections()?"); + } + if (!mAddSigningBlockRequest.isDone()) { + throw new IllegalStateException( + "APK Signing Block addition of signature(s) requested by" + + " outputZipSections() hasn't been fulfilled yet"); + } + mAddSigningBlockRequest = null; + mV2SignaturePending = false; + mV3SignaturePending = false; + } + + private void checkOutputApkNotDebuggableIfDebuggableMustBeRejected() throws SignatureException { + if (mDebuggableApkPermitted) { + return; + } + + try { + if (isOutputApkDebuggable()) { + throw new SignatureException( + "APK is debuggable (see android:debuggable attribute) and this engine is" + + " configured to refuse to sign debuggable APKs"); + } + } catch (ApkFormatException e) { + throw new SignatureException("Failed to determine whether the APK is debuggable", e); + } + } + + /** + * Returns whether the output APK is debuggable according to its {@code android:debuggable} + * declaration. + */ + private boolean isOutputApkDebuggable() throws ApkFormatException { + if (mDebuggable != null) { + return mDebuggable; + } + + if (mOutputAndroidManifestEntryDataRequest == null) { + throw new IllegalStateException( + "Cannot determine debuggable status of output APK because " + + ApkUtils.ANDROID_MANIFEST_ZIP_ENTRY_NAME + + " entry contents have not yet been requested"); + } + + if (!mOutputAndroidManifestEntryDataRequest.isDone()) { + throw new IllegalStateException( + "Still waiting to inspect output APK's " + + mOutputAndroidManifestEntryDataRequest.getEntryName()); + } + mDebuggable = + ApkUtils.getDebuggableFromBinaryAndroidManifest( + ByteBuffer.wrap(mOutputAndroidManifestEntryDataRequest.getData())); + return mDebuggable; + } + + private void forgetOutputApkDebuggableStatus() { + mDebuggable = null; + } + + /** Returns the output policy for the provided input JAR entry. */ + private InputJarEntryInstructions.OutputPolicy getInputJarEntryOutputPolicy(String entryName) { + if (mSignatureExpectedOutputJarEntryNames.contains(entryName)) { + return InputJarEntryInstructions.OutputPolicy.OUTPUT_BY_ENGINE; + } + if ((mOtherSignersSignaturesPreserved) + || (V1SchemeSigner.isJarEntryDigestNeededInManifest(entryName))) { + return InputJarEntryInstructions.OutputPolicy.OUTPUT; + } + return InputJarEntryInstructions.OutputPolicy.SKIP; + } + + private static class OutputJarSignatureRequestImpl implements OutputJarSignatureRequest { + private final List<JarEntry> mAdditionalJarEntries; + private volatile boolean mDone; + + private OutputJarSignatureRequestImpl(List<JarEntry> additionalZipEntries) { + mAdditionalJarEntries = + Collections.unmodifiableList(new ArrayList<>(additionalZipEntries)); + } + + @Override + public List<JarEntry> getAdditionalJarEntries() { + return mAdditionalJarEntries; + } + + @Override + public void done() { + mDone = true; + } + + private boolean isDone() { + return mDone; + } + } + + @SuppressWarnings("deprecation") + private static class OutputApkSigningBlockRequestImpl + implements OutputApkSigningBlockRequest, OutputApkSigningBlockRequest2 { + private final byte[] mApkSigningBlock; + private final int mPaddingBeforeApkSigningBlock; + private volatile boolean mDone; + + private OutputApkSigningBlockRequestImpl(byte[] apkSigingBlock, int paddingBefore) { + mApkSigningBlock = apkSigingBlock.clone(); + mPaddingBeforeApkSigningBlock = paddingBefore; + } + + @Override + public byte[] getApkSigningBlock() { + return mApkSigningBlock.clone(); + } + + @Override + public void done() { + mDone = true; + } + + private boolean isDone() { + return mDone; + } + + @Override + public int getPaddingSizeBeforeApkSigningBlock() { + return mPaddingBeforeApkSigningBlock; + } + } + + /** JAR entry inspection request which obtain the entry's uncompressed data. */ + private static class GetJarEntryDataRequest implements InspectJarEntryRequest { + private final String mEntryName; + private final Object mLock = new Object(); + + private boolean mDone; + private DataSink mDataSink; + private ByteArrayOutputStream mDataSinkBuf; + + private GetJarEntryDataRequest(String entryName) { + mEntryName = entryName; + } + + @Override + public String getEntryName() { + return mEntryName; + } + + @Override + public DataSink getDataSink() { + synchronized (mLock) { + checkNotDone(); + if (mDataSinkBuf == null) { + mDataSinkBuf = new ByteArrayOutputStream(); + } + if (mDataSink == null) { + mDataSink = DataSinks.asDataSink(mDataSinkBuf); + } + return mDataSink; + } + } + + @Override + public void done() { + synchronized (mLock) { + if (mDone) { + return; + } + mDone = true; + } + } + + private boolean isDone() { + synchronized (mLock) { + return mDone; + } + } + + private void checkNotDone() throws IllegalStateException { + synchronized (mLock) { + if (mDone) { + throw new IllegalStateException("Already done"); + } + } + } + + private byte[] getData() { + synchronized (mLock) { + if (!mDone) { + throw new IllegalStateException("Not yet done"); + } + return (mDataSinkBuf != null) ? mDataSinkBuf.toByteArray() : new byte[0]; + } + } + } + + /** JAR entry inspection request which obtains the digest of the entry's uncompressed data. */ + private static class GetJarEntryDataDigestRequest implements InspectJarEntryRequest { + private final String mEntryName; + private final String mJcaDigestAlgorithm; + private final Object mLock = new Object(); + + private boolean mDone; + private DataSink mDataSink; + private MessageDigest mMessageDigest; + private byte[] mDigest; + + private GetJarEntryDataDigestRequest(String entryName, String jcaDigestAlgorithm) { + mEntryName = entryName; + mJcaDigestAlgorithm = jcaDigestAlgorithm; + } + + @Override + public String getEntryName() { + return mEntryName; + } + + @Override + public DataSink getDataSink() { + synchronized (mLock) { + checkNotDone(); + if (mDataSink == null) { + mDataSink = DataSinks.asDataSink(getMessageDigest()); + } + return mDataSink; + } + } + + private MessageDigest getMessageDigest() { + synchronized (mLock) { + if (mMessageDigest == null) { + try { + mMessageDigest = MessageDigest.getInstance(mJcaDigestAlgorithm); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException( + mJcaDigestAlgorithm + " MessageDigest not available", e); + } + } + return mMessageDigest; + } + } + + @Override + public void done() { + synchronized (mLock) { + if (mDone) { + return; + } + mDone = true; + mDigest = getMessageDigest().digest(); + mMessageDigest = null; + mDataSink = null; + } + } + + private boolean isDone() { + synchronized (mLock) { + return mDone; + } + } + + private void checkNotDone() throws IllegalStateException { + synchronized (mLock) { + if (mDone) { + throw new IllegalStateException("Already done"); + } + } + } + + private byte[] getDigest() { + synchronized (mLock) { + if (!mDone) { + throw new IllegalStateException("Not yet done"); + } + return mDigest.clone(); + } + } + } + + /** JAR entry inspection request which transparently satisfies multiple such requests. */ + private static class CompoundInspectJarEntryRequest implements InspectJarEntryRequest { + private final String mEntryName; + private final InspectJarEntryRequest[] mRequests; + private final Object mLock = new Object(); + + private DataSink mSink; + + private CompoundInspectJarEntryRequest( + String entryName, InspectJarEntryRequest... requests) { + mEntryName = entryName; + mRequests = requests; + } + + @Override + public String getEntryName() { + return mEntryName; + } + + @Override + public DataSink getDataSink() { + synchronized (mLock) { + if (mSink == null) { + DataSink[] sinks = new DataSink[mRequests.length]; + for (int i = 0; i < sinks.length; i++) { + sinks[i] = mRequests[i].getDataSink(); + } + mSink = new TeeDataSink(sinks); + } + return mSink; + } + } + + @Override + public void done() { + for (InspectJarEntryRequest request : mRequests) { + request.done(); + } + } + } + + /** + * Configuration of a signer. + * + * <p>Use {@link Builder} to obtain configuration instances. + */ + public static class SignerConfig { + private final String mName; + private final PrivateKey mPrivateKey; + private final List<X509Certificate> mCertificates; + private final boolean mDeterministicDsaSigning; + private final int mMinSdkVersion; + private final boolean mSignerTargetsDevRelease; + private final SigningCertificateLineage mSigningCertificateLineage; + + private SignerConfig(Builder builder) { + mName = builder.mName; + mPrivateKey = builder.mPrivateKey; + mCertificates = Collections.unmodifiableList(new ArrayList<>(builder.mCertificates)); + mDeterministicDsaSigning = builder.mDeterministicDsaSigning; + mMinSdkVersion = builder.mMinSdkVersion; + mSignerTargetsDevRelease = builder.mSignerTargetsDevRelease; + mSigningCertificateLineage = builder.mSigningCertificateLineage; + } + + /** Returns the name of this signer. */ + public String getName() { + return mName; + } + + /** Returns the signing key of this signer. */ + public PrivateKey getPrivateKey() { + return mPrivateKey; + } + + /** + * Returns the certificate(s) of this signer. The first certificate's public key corresponds + * to this signer's private key. + */ + public List<X509Certificate> getCertificates() { + return mCertificates; + } + + /** + * If this signer is a DSA signer, whether or not the signing is done deterministically. + */ + public boolean getDeterministicDsaSigning() { + return mDeterministicDsaSigning; + } + + /** Returns the minimum SDK version for which this signer should be used. */ + public int getMinSdkVersion() { + return mMinSdkVersion; + } + + /** Returns whether this signer targets a development release. */ + public boolean getSignerTargetsDevRelease() { + return mSignerTargetsDevRelease; + } + + /** Returns the {@link SigningCertificateLineage} for this signer. */ + public SigningCertificateLineage getSigningCertificateLineage() { + return mSigningCertificateLineage; + } + + /** Builder of {@link SignerConfig} instances. */ + public static class Builder { + private final String mName; + private final PrivateKey mPrivateKey; + private final List<X509Certificate> mCertificates; + private final boolean mDeterministicDsaSigning; + private int mMinSdkVersion; + private boolean mSignerTargetsDevRelease; + private SigningCertificateLineage mSigningCertificateLineage; + + /** + * Constructs a new {@code Builder}. + * + * @param name signer's name. The name is reflected in the name of files comprising the + * JAR signature of the APK. + * @param privateKey signing key + * @param certificates list of one or more X.509 certificates. The subject public key of + * the first certificate must correspond to the {@code privateKey}. + */ + public Builder(String name, PrivateKey privateKey, List<X509Certificate> certificates) { + this(name, privateKey, certificates, false); + } + + /** + * Constructs a new {@code Builder}. + * + * @param name signer's name. The name is reflected in the name of files comprising the + * JAR signature of the APK. + * @param privateKey signing key + * @param certificates list of one or more X.509 certificates. The subject public key of + * the first certificate must correspond to the {@code privateKey}. + * @param deterministicDsaSigning When signing using DSA, whether or not the + * deterministic signing algorithm variant (RFC6979) should be used. + */ + public Builder(String name, PrivateKey privateKey, List<X509Certificate> certificates, + boolean deterministicDsaSigning) { + if (name.isEmpty()) { + throw new IllegalArgumentException("Empty name"); + } + mName = name; + mPrivateKey = privateKey; + mCertificates = new ArrayList<>(certificates); + mDeterministicDsaSigning = deterministicDsaSigning; + } + + /** @see #setLineageForMinSdkVersion(SigningCertificateLineage, int) */ + public Builder setMinSdkVersion(int minSdkVersion) { + return setLineageForMinSdkVersion(null, minSdkVersion); + } + + /** + * Sets the specified {@code minSdkVersion} as the minimum Android platform version + * (API level) for which the provided {@code lineage} (where applicable) should be used + * to produce the APK's signature. This method is useful if callers want to specify a + * particular rotated signer or lineage with restricted capabilities for later + * platform releases. + * + * <p><em>Note:</em>>The V1 and V2 signature schemes do not support key rotation and + * signing lineages with capabilities; only an app's original signer(s) can be used for + * the V1 and V2 signature blocks. Because of this, only a value of {@code + * minSdkVersion} >= 28 (Android P) where support for the V3 signature scheme was + * introduced can be specified. + * + * <p><em>Note:</em>Due to limitations with platform targeting in the V3.0 signature + * scheme, specifying a {@code minSdkVersion} value <= 32 (Android Sv2) will result in + * the current {@code SignerConfig} being used in the V3.0 signing block and applied to + * Android P through at least Sv2 (and later depending on the {@code minSdkVersion} for + * subsequent {@code SignerConfig} instances). Because of this, only a single {@code + * SignerConfig} can be instantiated with a minimum SDK version <= 32. + * + * @param lineage the {@code SigningCertificateLineage} to target the specified {@code + * minSdkVersion} + * @param minSdkVersion the minimum SDK version for which this {@code SignerConfig} + * should be used + * @return this {@code Builder} instance + * + * @throws IllegalArgumentException if the provided {@code minSdkVersion} < 28 or the + * certificate provided in the constructor is not in the specified {@code lineage}. + */ + public Builder setLineageForMinSdkVersion(SigningCertificateLineage lineage, + int minSdkVersion) { + if (minSdkVersion < AndroidSdkVersion.P) { + throw new IllegalArgumentException( + "SDK targeted signing config is only supported with the V3 signature " + + "scheme on Android P (SDK version " + + AndroidSdkVersion.P + ") and later"); + } + if (minSdkVersion < MIN_SDK_WITH_V31_SUPPORT) { + minSdkVersion = AndroidSdkVersion.P; + } + mMinSdkVersion = minSdkVersion; + // If a lineage is provided, ensure the signing certificate for this signer is in + // the lineage; in the case of multiple signing certificates, the first is always + // used in the lineage. + if (lineage != null && !lineage.isCertificateInLineage(mCertificates.get(0))) { + throw new IllegalArgumentException( + "The provided lineage does not contain the signing certificate, " + + mCertificates.get(0).getSubjectDN() + + ", for this SignerConfig"); + } + mSigningCertificateLineage = lineage; + return this; + } + + /** + * Sets whether this signer's min SDK version is intended to target a development + * release. + * + * <p>This is primarily required for a signer testing on a platform's development + * release; however, it is recommended that signer's use the latest development SDK + * version instead of explicitly specifying this boolean. This class will properly + * handle an SDK that is currently targeting a development release and will use the + * finalized SDK version on release. + */ + private Builder setSignerTargetsDevRelease(boolean signerTargetsDevRelease) { + if (signerTargetsDevRelease && mMinSdkVersion < MIN_SDK_WITH_V31_SUPPORT) { + throw new IllegalArgumentException( + "Rotation can only target a development release for signers targeting " + + MIN_SDK_WITH_V31_SUPPORT + " or later"); + } + mSignerTargetsDevRelease = signerTargetsDevRelease; + return this; + } + + + /** + * Returns a new {@code SignerConfig} instance configured based on the configuration of + * this builder. + */ + public SignerConfig build() { + return new SignerConfig(this); + } + } + } + + /** Builder of {@link DefaultApkSignerEngine} instances. */ + public static class Builder { + private List<SignerConfig> mSignerConfigs; + private List<SignerConfig> mTargetedSignerConfigs; + private SignerConfig mStampSignerConfig; + private SigningCertificateLineage mSourceStampSigningCertificateLineage; + private boolean mSourceStampTimestampEnabled = true; + private final int mMinSdkVersion; + + private boolean mV1SigningEnabled = true; + private boolean mV2SigningEnabled = true; + private boolean mV3SigningEnabled = true; + private int mRotationMinSdkVersion = V3SchemeConstants.DEFAULT_ROTATION_MIN_SDK_VERSION; + private boolean mRotationTargetsDevRelease = false; + private boolean mVerityEnabled = false; + private boolean mDebuggableApkPermitted = true; + private boolean mOtherSignersSignaturesPreserved; + private String mCreatedBy = "1.0 (Android)"; + + private SigningCertificateLineage mSigningCertificateLineage; + + // APK Signature Scheme v3 only supports a single signing certificate, so to move to v3 + // signing by default, but not require prior clients to update to explicitly disable v3 + // signing for multiple signers, we modify the mV3SigningEnabled depending on the provided + // inputs (multiple signers and mSigningCertificateLineage in particular). Maintain two + // extra variables to record whether or not mV3SigningEnabled has been set directly by a + // client and so should override the default behavior. + private boolean mV3SigningExplicitlyDisabled = false; + private boolean mV3SigningExplicitlyEnabled = false; + + /** + * Constructs a new {@code Builder}. + * + * @param signerConfigs information about signers with which the APK will be signed. At + * least one signer configuration must be provided. + * @param minSdkVersion API Level of the oldest Android platform on which the APK is + * supposed to be installed. See {@code minSdkVersion} attribute in the APK's {@code + * AndroidManifest.xml}. The higher the version, the stronger signing features will be + * enabled. + */ + public Builder(List<SignerConfig> signerConfigs, int minSdkVersion) { + if (signerConfigs.isEmpty()) { + throw new IllegalArgumentException("At least one signer config must be provided"); + } + if (signerConfigs.size() > 1) { + // APK Signature Scheme v3 only supports single signer, unless a + // SigningCertificateLineage is provided, in which case this will be reset to true, + // since we don't yet have a v4 scheme about which to worry + mV3SigningEnabled = false; + } + mSignerConfigs = new ArrayList<>(signerConfigs); + mMinSdkVersion = minSdkVersion; + } + + /** + * Sets the APK signature schemes that should be enabled based on the options provided by + * the caller. + */ + private void setEnabledSignatureSchemes() { + if (mV3SigningExplicitlyDisabled && mV3SigningExplicitlyEnabled) { + throw new IllegalStateException( + "Builder configured to both enable and disable APK " + + "Signature Scheme v3 signing"); + } + if (mV3SigningExplicitlyDisabled) { + mV3SigningEnabled = false; + } else if (mV3SigningExplicitlyEnabled) { + mV3SigningEnabled = true; + } + } + + /** + * Sets the SDK targeted signer configs based on the signing config and rotation options + * provided by the caller. + * + * @throws InvalidKeyException if a {@link SigningCertificateLineage} cannot be created + * from the provided options + */ + private void setTargetedSignerConfigs() throws InvalidKeyException { + // If the caller specified any SDK targeted signer configs, then the min SDK version + // should be set for those configs, all others should have a default 0 min SDK version. + mSignerConfigs.sort(((signerConfig1, signerConfig2) -> signerConfig1.getMinSdkVersion() + - signerConfig2.getMinSdkVersion())); + // With the signer configs sorted, find the first targeted signer config with a min + // SDK version > 0 to create the separate targeted signer configs. + mTargetedSignerConfigs = new ArrayList<>(); + for (int i = 0; i < mSignerConfigs.size(); i++) { + if (mSignerConfigs.get(i).getMinSdkVersion() > 0) { + mTargetedSignerConfigs = mSignerConfigs.subList(i, mSignerConfigs.size()); + mSignerConfigs = mSignerConfigs.subList(0, i); + break; + } + } + + // A lineage provided outside a targeted signing config is intended for the original + // rotation; sort the untargeted signing configs based on this lineage and create a new + // targeted signing config for the initial rotation. + if (mSigningCertificateLineage != null) { + if (!mTargetedSignerConfigs.isEmpty()) { + // Only the initial rotation can use the rotation-min-sdk-version; all + // subsequent targeted rotations must use targeted signing configs. + int firstTargetedSdkVersion = mTargetedSignerConfigs.get(0).getMinSdkVersion(); + if (mRotationMinSdkVersion >= firstTargetedSdkVersion) { + throw new IllegalStateException( + "The rotation-min-sdk-version, " + mRotationMinSdkVersion + + ", must be less than the first targeted SDK version, " + + firstTargetedSdkVersion); + } + } + try { + mSignerConfigs = mSigningCertificateLineage.sortSignerConfigs(mSignerConfigs); + } catch (IllegalArgumentException e) { + throw new IllegalStateException( + "Provided signer configs do not match the " + + "provided SigningCertificateLineage", + e); + } + // Get the last signer in the lineage, create a new targeted signer from it, + // and add it as a targeted signer config. + SignerConfig rotatedSignerConfig = mSignerConfigs.remove(mSignerConfigs.size() - 1); + SignerConfig.Builder rotatedConfigBuilder = new SignerConfig.Builder( + rotatedSignerConfig.getName(), rotatedSignerConfig.getPrivateKey(), + rotatedSignerConfig.getCertificates(), + rotatedSignerConfig.getDeterministicDsaSigning()); + rotatedConfigBuilder.setLineageForMinSdkVersion(mSigningCertificateLineage, + mRotationMinSdkVersion); + rotatedConfigBuilder.setSignerTargetsDevRelease(mRotationTargetsDevRelease); + mTargetedSignerConfigs.add(0, rotatedConfigBuilder.build()); + } + mSigningCertificateLineage = mergeTargetedSigningConfigLineages(); + } + + /** + * Merges and returns the lineages from any caller provided SDK targeted {@link + * SignerConfig} instances with an optional {@code lineage} specified as part of the general + * signing config. + * + * <p>If multiple signing configs target the same SDK version, or if any of the lineages + * cannot be merged, then an {@code IllegalStateException} is thrown. + */ + private SigningCertificateLineage mergeTargetedSigningConfigLineages() + throws InvalidKeyException { + SigningCertificateLineage mergedLineage = null; + int prevSdkVersion = 0; + for (SignerConfig signerConfig : mTargetedSignerConfigs) { + int signerMinSdkVersion = signerConfig.getMinSdkVersion(); + if (signerMinSdkVersion < AndroidSdkVersion.P) { + throw new IllegalStateException( + "Targeted signing config is not supported prior to SDK version " + + AndroidSdkVersion.P + "; received value " + + signerMinSdkVersion); + } + SigningCertificateLineage signerLineage = + signerConfig.getSigningCertificateLineage(); + // It is possible for a lineage to be null if the user is using one of the + // signers from the lineage as the only signer to target an SDK version; create + // a single element lineage to verify the signer is part of the merged lineage. + if (signerLineage == null) { + try { + signerLineage = new SigningCertificateLineage.Builder( + new SigningCertificateLineage.SignerConfig.Builder( + signerConfig.mPrivateKey, + signerConfig.mCertificates.get(0)) + .build()) + .build(); + } catch (CertificateEncodingException | NoSuchAlgorithmException + | SignatureException e) { + throw new IllegalStateException( + "Unable to create a SignerConfig for signer from certificate " + + signerConfig.mCertificates.get(0).getSubjectDN()); + } + } + // The V3.0 signature scheme does not support verified targeted SDK signing + // configs; if a signer is targeting any SDK version < T, then it will + // target P with the V3.0 signature scheme. + if (signerMinSdkVersion < AndroidSdkVersion.T) { + signerMinSdkVersion = AndroidSdkVersion.P; + } + // Ensure there are no SignerConfigs targeting the same SDK version. + if (signerMinSdkVersion == prevSdkVersion) { + throw new IllegalStateException( + "Multiple SignerConfigs were found targeting SDK version " + + signerMinSdkVersion); + } + // If multiple lineages have been provided, then verify each subsequent lineage + // is a valid descendant or ancestor of the previously merged lineages. + if (mergedLineage == null) { + mergedLineage = signerLineage; + } else { + try { + mergedLineage = mergedLineage.mergeLineageWith(signerLineage); + } catch (IllegalArgumentException e) { + throw new IllegalStateException( + "The provided lineage targeting SDK " + signerMinSdkVersion + + " is not in the signing history of the other targeted " + + "signing configs", e); + } + } + prevSdkVersion = signerMinSdkVersion; + } + return mergedLineage; + } + + /** + * Returns a new {@code DefaultApkSignerEngine} instance configured based on the + * configuration of this builder. + */ + public DefaultApkSignerEngine build() throws InvalidKeyException { + setEnabledSignatureSchemes(); + setTargetedSignerConfigs(); + + // make sure our signers are appropriately setup + if (mSigningCertificateLineage != null) { + if (!mV3SigningEnabled && mSignerConfigs.size() > 1) { + // this is a strange situation: we've provided a valid rotation history, but + // are only signing with v1/v2. blow up, since we don't know for sure with + // which signer the user intended to sign + throw new IllegalStateException( + "Provided multiple signers which are part of the" + + " SigningCertificateLineage, but not signing with APK" + + " Signature Scheme v3"); + } + } else if (mV3SigningEnabled && mSignerConfigs.size() > 1) { + throw new IllegalStateException( + "Multiple signing certificates provided for use with APK Signature Scheme" + + " v3 without an accompanying SigningCertificateLineage"); + } + + return new DefaultApkSignerEngine( + mSignerConfigs, + mTargetedSignerConfigs, + mStampSignerConfig, + mSourceStampSigningCertificateLineage, + mSourceStampTimestampEnabled, + mMinSdkVersion, + mV1SigningEnabled, + mV2SigningEnabled, + mV3SigningEnabled, + mVerityEnabled, + mDebuggableApkPermitted, + mOtherSignersSignaturesPreserved, + mCreatedBy, + mSigningCertificateLineage); + } + + /** Sets the signer configuration for the SourceStamp to be embedded in the APK. */ + public Builder setStampSignerConfig(SignerConfig stampSignerConfig) { + mStampSignerConfig = stampSignerConfig; + return this; + } + + /** + * Sets the source stamp {@link SigningCertificateLineage}. This structure provides proof of + * signing certificate rotation for certificates previously used to sign source stamps. + */ + public Builder setSourceStampSigningCertificateLineage( + SigningCertificateLineage sourceStampSigningCertificateLineage) { + mSourceStampSigningCertificateLineage = sourceStampSigningCertificateLineage; + return this; + } + + /** + * Sets whether the source stamp should contain the timestamp attribute with the time + * at which the source stamp was signed. + */ + public Builder setSourceStampTimestampEnabled(boolean value) { + mSourceStampTimestampEnabled = value; + return this; + } + + /** + * Sets whether the APK should be signed using JAR signing (aka v1 signature scheme). + * + * <p>By default, the APK will be signed using this scheme. + */ + public Builder setV1SigningEnabled(boolean enabled) { + mV1SigningEnabled = enabled; + return this; + } + + /** + * Sets whether the APK should be signed using APK Signature Scheme v2 (aka v2 signature + * scheme). + * + * <p>By default, the APK will be signed using this scheme. + */ + public Builder setV2SigningEnabled(boolean enabled) { + mV2SigningEnabled = enabled; + return this; + } + + /** + * Sets whether the APK should be signed using APK Signature Scheme v3 (aka v3 signature + * scheme). + * + * <p>By default, the APK will be signed using this scheme. + */ + public Builder setV3SigningEnabled(boolean enabled) { + mV3SigningEnabled = enabled; + if (enabled) { + mV3SigningExplicitlyEnabled = true; + } else { + mV3SigningExplicitlyDisabled = true; + } + return this; + } + + /** + * Sets whether the APK should be signed using the verity signature algorithm in the v2 and + * v3 signature blocks. + * + * <p>By default, the APK will be signed using the verity signature algorithm for the v2 and + * v3 signature schemes. + */ + public Builder setVerityEnabled(boolean enabled) { + mVerityEnabled = enabled; + return this; + } + + /** + * Sets whether the APK should be signed even if it is marked as debuggable ({@code + * android:debuggable="true"} in its {@code AndroidManifest.xml}). For backward + * compatibility reasons, the default value of this setting is {@code true}. + * + * <p>It is dangerous to sign debuggable APKs with production/release keys because Android + * platform loosens security checks for such APKs. For example, arbitrary unauthorized code + * may be executed in the context of such an app by anybody with ADB shell access. + */ + public Builder setDebuggableApkPermitted(boolean permitted) { + mDebuggableApkPermitted = permitted; + return this; + } + + /** + * Sets whether signatures produced by signers other than the ones configured in this engine + * should be copied from the input APK to the output APK. + * + * <p>By default, signatures of other signers are omitted from the output APK. + */ + public Builder setOtherSignersSignaturesPreserved(boolean preserved) { + mOtherSignersSignaturesPreserved = preserved; + return this; + } + + /** Sets the value of the {@code Created-By} field in JAR signature files. */ + public Builder setCreatedBy(String createdBy) { + if (createdBy == null) { + throw new NullPointerException(); + } + mCreatedBy = createdBy; + return this; + } + + /** + * Sets the {@link SigningCertificateLineage} to use with the v3 signature scheme. This + * structure provides proof of signing certificate rotation linking {@link SignerConfig} + * objects to previous ones. + */ + public Builder setSigningCertificateLineage( + SigningCertificateLineage signingCertificateLineage) { + if (signingCertificateLineage != null) { + mV3SigningEnabled = true; + mSigningCertificateLineage = signingCertificateLineage; + } + return this; + } + + /** + * Sets the minimum Android platform version (API Level) for which an APK's rotated signing + * key should be used to produce the APK's signature. The original signing key for the APK + * will be used for all previous platform versions. If a rotated key with signing lineage is + * not provided then this method is a noop. + * + * <p>By default, if a signing lineage is specified with {@link + * #setSigningCertificateLineage(SigningCertificateLineage)}, then the APK Signature Scheme + * V3.1 will be used to only apply the rotation on devices running Android T+. + * + * <p><em>Note:</em>Specifying a {@code minSdkVersion} value <= 32 (Android Sv2) will result + * in the original V3 signing block being used without platform targeting. + */ + public Builder setMinSdkVersionForRotation(int minSdkVersion) { + // If the provided SDK version does not support v3.1, then use the default SDK version + // with rotation support. + if (minSdkVersion < MIN_SDK_WITH_V31_SUPPORT) { + mRotationMinSdkVersion = MIN_SDK_WITH_V3_SUPPORT; + } else { + mRotationMinSdkVersion = minSdkVersion; + } + return this; + } + + /** + * Sets whether the rotation-min-sdk-version is intended to target a development release; + * this is primarily required after the T SDK is finalized, and an APK needs to target U + * during its development cycle for rotation. + * + * <p>This is only required after the T SDK is finalized since S and earlier releases do + * not know about the V3.1 block ID, but once T is released and work begins on U, U will + * use the SDK version of T during development. Specifying a rotation-min-sdk-version of T's + * SDK version along with setting {@code enabled} to true will allow an APK to use the + * rotated key on a device running U while causing this to be bypassed for T. + * + * <p><em>Note:</em>If the rotation-min-sdk-version is less than or equal to 32 (Android + * Sv2), then the rotated signing key will be used in the v3.0 signing block and this call + * will be a noop. + */ + public Builder setRotationTargetsDevRelease(boolean enabled) { + mRotationTargetsDevRelease = enabled; + return this; + } + } +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/Hints.java b/platform/android/java/editor/src/main/java/com/android/apksig/Hints.java new file mode 100644 index 0000000000..4070fa231a --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/Hints.java @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.apksig; +import java.io.IOException; +import java.io.DataOutputStream; +import java.io.ByteArrayOutputStream; +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public final class Hints { + /** + * Name of hint pattern asset file in APK. + */ + public static final String PIN_HINT_ASSET_ZIP_ENTRY_NAME = "assets/com.android.hints.pins.txt"; + + /** + * Name of hint byte range data file in APK. Keep in sync with PinnerService.java. + */ + public static final String PIN_BYTE_RANGE_ZIP_ENTRY_NAME = "pinlist.meta"; + + private static int clampToInt(long value) { + return (int) Math.max(0, Math.min(value, Integer.MAX_VALUE)); + } + + public static final class ByteRange { + final long start; + final long end; + + public ByteRange(long start, long end) { + this.start = start; + this.end = end; + } + } + + public static final class PatternWithRange { + final Pattern pattern; + final long offset; + final long size; + + public PatternWithRange(String pattern) { + this.pattern = Pattern.compile(pattern); + this.offset= 0; + this.size = Long.MAX_VALUE; + } + + public PatternWithRange(String pattern, long offset, long size) { + this.pattern = Pattern.compile(pattern); + this.offset = offset; + this.size = size; + } + + public Matcher matcher(CharSequence input) { + return this.pattern.matcher(input); + } + + public ByteRange ClampToAbsoluteByteRange(ByteRange rangeIn) { + if (rangeIn.end - rangeIn.start < this.offset) { + return null; + } + long rangeOutStart = rangeIn.start + this.offset; + long rangeOutSize = Math.min(rangeIn.end - rangeOutStart, + this.size); + return new ByteRange(rangeOutStart, + rangeOutStart + rangeOutSize); + } + } + + /** + * Create a blob of bytes that PinnerService understands as a + * sequence of byte ranges to pin. + */ + public static byte[] encodeByteRangeList(List<ByteRange> pinByteRanges) { + ByteArrayOutputStream bos = new ByteArrayOutputStream(pinByteRanges.size() * 8); + DataOutputStream out = new DataOutputStream(bos); + try { + for (ByteRange pinByteRange : pinByteRanges) { + out.writeInt(clampToInt(pinByteRange.start)); + out.writeInt(clampToInt(pinByteRange.end - pinByteRange.start)); + } + } catch (IOException ex) { + throw new AssertionError("impossible", ex); + } + return bos.toByteArray(); + } + + public static ArrayList<PatternWithRange> parsePinPatterns(byte[] patternBlob) { + ArrayList<PatternWithRange> pinPatterns = new ArrayList<>(); + try { + for (String rawLine : new String(patternBlob, "UTF-8").split("\n")) { + String line = rawLine.replaceFirst("#.*", ""); // # starts a comment + String[] fields = line.split(" "); + if (fields.length == 1) { + pinPatterns.add(new PatternWithRange(fields[0])); + } else if (fields.length == 3) { + long start = Long.parseLong(fields[1]); + long end = Long.parseLong(fields[2]); + pinPatterns.add(new PatternWithRange(fields[0], start, end - start)); + } else { + throw new AssertionError("bad pin pattern line " + line); + } + } + } catch (UnsupportedEncodingException ex) { + throw new RuntimeException("UTF-8 must be supported", ex); + } + return pinPatterns; + } +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/README.md b/platform/android/java/editor/src/main/java/com/android/apksig/README.md new file mode 100644 index 0000000000..a89b848909 --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/README.md @@ -0,0 +1,32 @@ +# apksig ([commit ac5cbb07d87cc342fcf07715857a812305d69888](https://android.googlesource.com/platform/tools/apksig/+/ac5cbb07d87cc342fcf07715857a812305d69888)) + +apksig is a project which aims to simplify APK signing and checking whether APK signatures are +expected to verify on Android. apksig supports +[JAR signing](https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File) +(used by Android since day one) and +[APK Signature Scheme v2](https://source.android.com/security/apksigning/v2.html) (supported since +Android Nougat, API Level 24). apksig is meant to be used outside of Android devices. + +The key feature of apksig is that it knows about differences in APK signature verification logic +between different versions of the Android platform. apksig thus thoroughly checks whether an APK's +signature is expected to verify on all Android platform versions supported by the APK. When signing +an APK, apksig chooses the most appropriate cryptographic algorithms based on the Android platform +versions supported by the APK being signed. + +## apksig library + +apksig library offers three primitives: + +* `ApkSigner` which signs the provided APK so that it verifies on all Android platform versions + supported by the APK. The range of platform versions can be customized. +* `ApkVerifier` which checks whether the provided APK is expected to verify on all Android + platform versions supported by the APK. The range of platform versions can be customized. +* `(Default)ApkSignerEngine` which abstracts away signing APKs from parsing and building APKs. + This is useful in optimized APK building pipelines, such as in Android Plugin for Gradle, + which need to perform signing while building an APK, instead of after. For simpler use cases + where the APK to be signed is available upfront, the `ApkSigner` above is easier to use. + +_NOTE: Some public classes of the library are in packages having the word "internal" in their name. +These are not public API of the library. Do not use \*.internal.\* classes directly because these +classes may change any time without regard to existing clients outside of `apksig` and `apksigner`._ + diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/SigningCertificateLineage.java b/platform/android/java/editor/src/main/java/com/android/apksig/SigningCertificateLineage.java new file mode 100644 index 0000000000..0f1cc33c98 --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/SigningCertificateLineage.java @@ -0,0 +1,1325 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig; + +import static com.android.apksig.internal.apk.ApkSigningBlockUtils.getLengthPrefixedSlice; + +import com.android.apksig.apk.ApkFormatException; +import com.android.apksig.apk.ApkUtils; +import com.android.apksig.internal.apk.ApkSigningBlockUtils; +import com.android.apksig.internal.apk.SignatureAlgorithm; +import com.android.apksig.internal.apk.SignatureInfo; +import com.android.apksig.internal.apk.v3.V3SchemeConstants; +import com.android.apksig.internal.apk.v3.V3SchemeSigner; +import com.android.apksig.internal.apk.v3.V3SigningCertificateLineage; +import com.android.apksig.internal.apk.v3.V3SigningCertificateLineage.SigningCertificateNode; +import com.android.apksig.internal.util.AndroidSdkVersion; +import com.android.apksig.internal.util.ByteBufferUtils; +import com.android.apksig.internal.util.Pair; +import com.android.apksig.internal.util.RandomAccessFileDataSink; +import com.android.apksig.util.DataSink; +import com.android.apksig.util.DataSource; +import com.android.apksig.util.DataSources; +import com.android.apksig.zip.ZipFormatException; + +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SignatureException; +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * APK Signer Lineage. + * + * <p>The signer lineage contains a history of signing certificates with each ancestor attesting to + * the validity of its descendant. Each additional descendant represents a new identity that can be + * used to sign an APK, and each generation has accompanying attributes which represent how the + * APK would like to view the older signing certificates, specifically how they should be trusted in + * certain situations. + * + * <p> Its primary use is to enable APK Signing Certificate Rotation. The Android platform verifies + * the APK Signer Lineage, and if the current signing certificate for the APK is in the Signer + * Lineage, and the Lineage contains the certificate the platform associates with the APK, it will + * allow upgrades to the new certificate. + * + * @see <a href="https://source.android.com/security/apksigning/index.html">Application Signing</a> + */ +public class SigningCertificateLineage { + + public final static int MAGIC = 0x3eff39d1; + + private final static int FIRST_VERSION = 1; + + private static final int CURRENT_VERSION = FIRST_VERSION; + + /** accept data from already installed pkg with this cert */ + private static final int PAST_CERT_INSTALLED_DATA = 1; + + /** accept sharedUserId with pkg with this cert */ + private static final int PAST_CERT_SHARED_USER_ID = 2; + + /** grant SIGNATURE permissions to pkgs with this cert */ + private static final int PAST_CERT_PERMISSION = 4; + + /** + * Enable updates back to this certificate. WARNING: this effectively removes any benefit of + * signing certificate changes, since a compromised key could retake control of an app even + * after change, and should only be used if there is a problem encountered when trying to ditch + * an older cert. + */ + private static final int PAST_CERT_ROLLBACK = 8; + + /** + * Preserve authenticator module-based access in AccountManager gated by signing certificate. + */ + private static final int PAST_CERT_AUTH = 16; + + private final int mMinSdkVersion; + + /** + * The signing lineage is just a list of nodes, with the first being the original signing + * certificate and the most recent being the one with which the APK is to actually be signed. + */ + private final List<SigningCertificateNode> mSigningLineage; + + private SigningCertificateLineage(int minSdkVersion, List<SigningCertificateNode> list) { + mMinSdkVersion = minSdkVersion; + mSigningLineage = list; + } + + /** + * Creates a {@code SigningCertificateLineage} with a single signer in the lineage. + */ + private static SigningCertificateLineage createSigningLineage(int minSdkVersion, + SignerConfig signer, SignerCapabilities capabilities) { + SigningCertificateLineage signingCertificateLineage = new SigningCertificateLineage( + minSdkVersion, new ArrayList<>()); + return signingCertificateLineage.spawnFirstDescendant(signer, capabilities); + } + + private static SigningCertificateLineage createSigningLineage( + int minSdkVersion, SignerConfig parent, SignerCapabilities parentCapabilities, + SignerConfig child, SignerCapabilities childCapabilities) + throws CertificateEncodingException, InvalidKeyException, NoSuchAlgorithmException, + SignatureException { + SigningCertificateLineage signingCertificateLineage = + new SigningCertificateLineage(minSdkVersion, new ArrayList<>()); + signingCertificateLineage = + signingCertificateLineage.spawnFirstDescendant(parent, parentCapabilities); + return signingCertificateLineage.spawnDescendant(parent, child, childCapabilities); + } + + public static SigningCertificateLineage readFromBytes(byte[] lineageBytes) + throws IOException { + return readFromDataSource(DataSources.asDataSource(ByteBuffer.wrap(lineageBytes))); + } + + public static SigningCertificateLineage readFromFile(File file) + throws IOException { + if (file == null) { + throw new NullPointerException("file == null"); + } + RandomAccessFile inputFile = new RandomAccessFile(file, "r"); + return readFromDataSource(DataSources.asDataSource(inputFile)); + } + + public static SigningCertificateLineage readFromDataSource(DataSource dataSource) + throws IOException { + if (dataSource == null) { + throw new NullPointerException("dataSource == null"); + } + ByteBuffer inBuff = dataSource.getByteBuffer(0, (int) dataSource.size()); + inBuff.order(ByteOrder.LITTLE_ENDIAN); + return read(inBuff); + } + + /** + * Extracts a Signing Certificate Lineage from a v3 signer proof-of-rotation attribute. + * + * <note> + * this may not give a complete representation of an APK's signing certificate history, + * since the APK may have multiple signers corresponding to different platform versions. + * Use <code> readFromApkFile</code> to handle this case. + * </note> + * @param attrValue + */ + public static SigningCertificateLineage readFromV3AttributeValue(byte[] attrValue) + throws IOException { + List<SigningCertificateNode> parsedLineage = + V3SigningCertificateLineage.readSigningCertificateLineage(ByteBuffer.wrap( + attrValue).order(ByteOrder.LITTLE_ENDIAN)); + int minSdkVersion = calculateMinSdkVersion(parsedLineage); + return new SigningCertificateLineage(minSdkVersion, parsedLineage); + } + + /** + * Extracts a Signing Certificate Lineage from the proof-of-rotation attribute in the V3 + * signature block of the provided APK File. + * + * @throws IllegalArgumentException if the provided APK does not contain a V3 signature block, + * or if the V3 signature block does not contain a valid lineage. + */ + public static SigningCertificateLineage readFromApkFile(File apkFile) + throws IOException, ApkFormatException { + try (RandomAccessFile f = new RandomAccessFile(apkFile, "r")) { + DataSource apk = DataSources.asDataSource(f, 0, f.length()); + return readFromApkDataSource(apk); + } + } + + /** + * Extracts a Signing Certificate Lineage from the proof-of-rotation attribute in the V3 and + * V3.1 signature blocks of the provided APK DataSource. + * + * @throws IllegalArgumentException if the provided APK does not contain a V3 nor V3.1 + * signature block, or if the V3 and V3.1 signature blocks do not contain a valid lineage. + */ + + public static SigningCertificateLineage readFromApkDataSource(DataSource apk) + throws IOException, ApkFormatException { + return readFromApkDataSource(apk, /* readV31Lineage= */ true, /* readV3Lineage= */true); + } + + /** + * Extracts a Signing Certificate Lineage from the proof-of-rotation attribute in the V3.1 + * signature blocks of the provided APK DataSource. + * + * @throws IllegalArgumentException if the provided APK does not contain a V3.1 signature block, + * or if the V3.1 signature block does not contain a valid lineage. + */ + + public static SigningCertificateLineage readV31FromApkDataSource(DataSource apk) + throws IOException, ApkFormatException { + return readFromApkDataSource(apk, /* readV31Lineage= */ true, + /* readV3Lineage= */ false); + } + + private static SigningCertificateLineage readFromApkDataSource( + DataSource apk, + boolean readV31Lineage, + boolean readV3Lineage) + throws IOException, ApkFormatException { + ApkUtils.ZipSections zipSections; + try { + zipSections = ApkUtils.findZipSections(apk); + } catch (ZipFormatException e) { + throw new ApkFormatException(e.getMessage()); + } + + List<SignatureInfo> signatureInfoList = new ArrayList<>(); + if (readV31Lineage) { + try { + ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result( + ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V31); + signatureInfoList.add( + ApkSigningBlockUtils.findSignature(apk, zipSections, + V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID, result)); + } catch (ApkSigningBlockUtils.SignatureNotFoundException ignored) { + // This could be expected if there's only a V3 signature block. + } + } + if (readV3Lineage) { + try { + ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result( + ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3); + signatureInfoList.add( + ApkSigningBlockUtils.findSignature(apk, zipSections, + V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID, result)); + } catch (ApkSigningBlockUtils.SignatureNotFoundException ignored) { + // This could be expected if the provided APK is not signed with the V3 signature + // scheme + } + } + if (signatureInfoList.isEmpty()) { + String message; + if (readV31Lineage && readV3Lineage) { + message = "The provided APK does not contain a valid V3 nor V3.1 signature block."; + } else if (readV31Lineage) { + message = "The provided APK does not contain a valid V3.1 signature block."; + } else if (readV3Lineage) { + message = "The provided APK does not contain a valid V3 signature block."; + } else { + message = "No signature blocks were requested."; + } + throw new IllegalArgumentException(message); + } + + List<SigningCertificateLineage> lineages = new ArrayList<>(1); + for (SignatureInfo signatureInfo : signatureInfoList) { + // FORMAT: + // * length-prefixed sequence of length-prefixed signers: + // * length-prefixed signed data + // * minSDK + // * maxSDK + // * length-prefixed sequence of length-prefixed signatures + // * length-prefixed public key + ByteBuffer signers = getLengthPrefixedSlice(signatureInfo.signatureBlock); + while (signers.hasRemaining()) { + ByteBuffer signer = getLengthPrefixedSlice(signers); + ByteBuffer signedData = getLengthPrefixedSlice(signer); + try { + SigningCertificateLineage lineage = readFromSignedData(signedData); + lineages.add(lineage); + } catch (IllegalArgumentException ignored) { + // The current signer block does not contain a valid lineage, but it is possible + // another block will. + } + } + } + + SigningCertificateLineage result; + if (lineages.isEmpty()) { + throw new IllegalArgumentException( + "The provided APK does not contain a valid lineage."); + } else if (lineages.size() > 1) { + result = consolidateLineages(lineages); + } else { + result = lineages.get(0); + } + return result; + } + + /** + * Extracts a Signing Certificate Lineage from the proof-of-rotation attribute in the provided + * signed data portion of a signer in a V3 signature block. + * + * @throws IllegalArgumentException if the provided signed data does not contain a valid + * lineage. + */ + public static SigningCertificateLineage readFromSignedData(ByteBuffer signedData) + throws IOException, ApkFormatException { + // FORMAT: + // * length-prefixed sequence of length-prefixed digests: + // * length-prefixed sequence of certificates: + // * length-prefixed bytes: X.509 certificate (ASN.1 DER encoded). + // * uint-32: minSdkVersion + // * uint-32: maxSdkVersion + // * length-prefixed sequence of length-prefixed additional attributes: + // * uint32: ID + // * (length - 4) bytes: value + // * uint32: Proof-of-rotation ID: 0x3ba06f8c + // * length-prefixed proof-of-rotation structure + // consume the digests through the maxSdkVersion to reach the lineage in the attributes + getLengthPrefixedSlice(signedData); + getLengthPrefixedSlice(signedData); + signedData.getInt(); + signedData.getInt(); + // iterate over the additional attributes adding any lineages to the List + ByteBuffer additionalAttributes = getLengthPrefixedSlice(signedData); + List<SigningCertificateLineage> lineages = new ArrayList<>(1); + while (additionalAttributes.hasRemaining()) { + ByteBuffer attribute = getLengthPrefixedSlice(additionalAttributes); + int id = attribute.getInt(); + if (id == V3SchemeConstants.PROOF_OF_ROTATION_ATTR_ID) { + byte[] value = ByteBufferUtils.toByteArray(attribute); + SigningCertificateLineage lineage = readFromV3AttributeValue(value); + lineages.add(lineage); + } + } + SigningCertificateLineage result; + // There should only be a single attribute with the lineage, but if there are multiple then + // attempt to consolidate the lineages. + if (lineages.isEmpty()) { + throw new IllegalArgumentException("The signed data does not contain a valid lineage."); + } else if (lineages.size() > 1) { + result = consolidateLineages(lineages); + } else { + result = lineages.get(0); + } + return result; + } + + public byte[] getBytes() { + return write().array(); + } + + public void writeToFile(File file) throws IOException { + if (file == null) { + throw new NullPointerException("file == null"); + } + RandomAccessFile outputFile = new RandomAccessFile(file, "rw"); + writeToDataSink(new RandomAccessFileDataSink(outputFile)); + } + + public void writeToDataSink(DataSink dataSink) throws IOException { + if (dataSink == null) { + throw new NullPointerException("dataSink == null"); + } + dataSink.consume(write()); + } + + /** + * Add a new signing certificate to the lineage. This effectively creates a signing certificate + * rotation event, forcing APKs which include this lineage to be signed by the new signer. The + * flags associated with the new signer are set to a default value. + * + * @param parent current signing certificate of the containing APK + * @param child new signing certificate which will sign the APK contents + */ + public SigningCertificateLineage spawnDescendant(SignerConfig parent, SignerConfig child) + throws CertificateEncodingException, InvalidKeyException, NoSuchAlgorithmException, + SignatureException { + if (parent == null || child == null) { + throw new NullPointerException("can't add new descendant to lineage with null inputs"); + } + SignerCapabilities signerCapabilities = new SignerCapabilities.Builder().build(); + return spawnDescendant(parent, child, signerCapabilities); + } + + /** + * Add a new signing certificate to the lineage. This effectively creates a signing certificate + * rotation event, forcing APKs which include this lineage to be signed by the new signer. + * + * @param parent current signing certificate of the containing APK + * @param child new signing certificate which will sign the APK contents + * @param childCapabilities flags + */ + public SigningCertificateLineage spawnDescendant( + SignerConfig parent, SignerConfig child, SignerCapabilities childCapabilities) + throws CertificateEncodingException, InvalidKeyException, + NoSuchAlgorithmException, SignatureException { + if (parent == null) { + throw new NullPointerException("parent == null"); + } + if (child == null) { + throw new NullPointerException("child == null"); + } + if (childCapabilities == null) { + throw new NullPointerException("childCapabilities == null"); + } + if (mSigningLineage.isEmpty()) { + throw new IllegalArgumentException("Cannot spawn descendant signing certificate on an" + + " empty SigningCertificateLineage: no parent node"); + } + + // make sure that the parent matches our newest generation (leaf node/sink) + SigningCertificateNode currentGeneration = mSigningLineage.get(mSigningLineage.size() - 1); + if (!Arrays.equals(currentGeneration.signingCert.getEncoded(), + parent.getCertificate().getEncoded())) { + throw new IllegalArgumentException("SignerConfig Certificate containing private key" + + " to sign the new SigningCertificateLineage record does not match the" + + " existing most recent record"); + } + + // create data to be signed, including the algorithm we're going to use + SignatureAlgorithm signatureAlgorithm = getSignatureAlgorithm(parent); + ByteBuffer prefixedSignedData = ByteBuffer.wrap( + V3SigningCertificateLineage.encodeSignedData( + child.getCertificate(), signatureAlgorithm.getId())); + prefixedSignedData.position(4); + ByteBuffer signedDataBuffer = ByteBuffer.allocate(prefixedSignedData.remaining()); + signedDataBuffer.put(prefixedSignedData); + byte[] signedData = signedDataBuffer.array(); + + // create SignerConfig to do the signing + List<X509Certificate> certificates = new ArrayList<>(1); + certificates.add(parent.getCertificate()); + ApkSigningBlockUtils.SignerConfig newSignerConfig = + new ApkSigningBlockUtils.SignerConfig(); + newSignerConfig.privateKey = parent.getPrivateKey(); + newSignerConfig.certificates = certificates; + newSignerConfig.signatureAlgorithms = Collections.singletonList(signatureAlgorithm); + + // sign it + List<Pair<Integer, byte[]>> signatures = + ApkSigningBlockUtils.generateSignaturesOverData(newSignerConfig, signedData); + + // finally, add it to our lineage + SignatureAlgorithm sigAlgorithm = SignatureAlgorithm.findById(signatures.get(0).getFirst()); + byte[] signature = signatures.get(0).getSecond(); + currentGeneration.sigAlgorithm = sigAlgorithm; + SigningCertificateNode childNode = + new SigningCertificateNode( + child.getCertificate(), sigAlgorithm, null, + signature, childCapabilities.getFlags()); + List<SigningCertificateNode> lineageCopy = new ArrayList<>(mSigningLineage); + lineageCopy.add(childNode); + return new SigningCertificateLineage(mMinSdkVersion, lineageCopy); + } + + /** + * The number of signing certificates in the lineage, including the current signer, which means + * this value can also be used to V2determine the number of signing certificate rotations by + * subtracting 1. + */ + public int size() { + return mSigningLineage.size(); + } + + private SignatureAlgorithm getSignatureAlgorithm(SignerConfig parent) + throws InvalidKeyException { + PublicKey publicKey = parent.getCertificate().getPublicKey(); + + // TODO switch to one signature algorithm selection, or add support for multiple algorithms + List<SignatureAlgorithm> algorithms = V3SchemeSigner.getSuggestedSignatureAlgorithms( + publicKey, mMinSdkVersion, false /* verityEnabled */, + false /* deterministicDsaSigning */); + return algorithms.get(0); + } + + private SigningCertificateLineage spawnFirstDescendant( + SignerConfig parent, SignerCapabilities signerCapabilities) { + if (!mSigningLineage.isEmpty()) { + throw new IllegalStateException("SigningCertificateLineage already has its first node"); + } + + // check to make sure that the public key for the first node is acceptable for our minSdk + try { + getSignatureAlgorithm(parent); + } catch (InvalidKeyException e) { + throw new IllegalArgumentException("Algorithm associated with first signing certificate" + + " invalid on desired platform versions", e); + } + + // create "fake" signed data (there will be no signature over it, since there is no parent + SigningCertificateNode firstNode = new SigningCertificateNode( + parent.getCertificate(), null, null, new byte[0], signerCapabilities.getFlags()); + return new SigningCertificateLineage(mMinSdkVersion, Collections.singletonList(firstNode)); + } + + private static SigningCertificateLineage read(ByteBuffer inputByteBuffer) + throws IOException { + ApkSigningBlockUtils.checkByteOrderLittleEndian(inputByteBuffer); + if (inputByteBuffer.remaining() < 8) { + throw new IllegalArgumentException( + "Improper SigningCertificateLineage format: insufficient data for header."); + } + + if (inputByteBuffer.getInt() != MAGIC) { + throw new IllegalArgumentException( + "Improper SigningCertificateLineage format: MAGIC header mismatch."); + } + return read(inputByteBuffer, inputByteBuffer.getInt()); + } + + private static SigningCertificateLineage read(ByteBuffer inputByteBuffer, int version) + throws IOException { + switch (version) { + case FIRST_VERSION: + try { + List<SigningCertificateNode> nodes = + V3SigningCertificateLineage.readSigningCertificateLineage( + getLengthPrefixedSlice(inputByteBuffer)); + int minSdkVersion = calculateMinSdkVersion(nodes); + return new SigningCertificateLineage(minSdkVersion, nodes); + } catch (ApkFormatException e) { + // unable to get a proper length-prefixed lineage slice + throw new IOException("Unable to read list of signing certificate nodes in " + + "SigningCertificateLineage", e); + } + default: + throw new IllegalArgumentException( + "Improper SigningCertificateLineage format: unrecognized version."); + } + } + + private static int calculateMinSdkVersion(List<SigningCertificateNode> nodes) { + if (nodes == null) { + throw new IllegalArgumentException("Can't calculate minimum SDK version of null nodes"); + } + int minSdkVersion = AndroidSdkVersion.P; // lineage introduced in P + for (SigningCertificateNode node : nodes) { + if (node.sigAlgorithm != null) { + int nodeMinSdkVersion = node.sigAlgorithm.getMinSdkVersion(); + if (nodeMinSdkVersion > minSdkVersion) { + minSdkVersion = nodeMinSdkVersion; + } + } + } + return minSdkVersion; + } + + private ByteBuffer write() { + byte[] encodedLineage = + V3SigningCertificateLineage.encodeSigningCertificateLineage(mSigningLineage); + int payloadSize = 4 + 4 + 4 + encodedLineage.length; + ByteBuffer result = ByteBuffer.allocate(payloadSize); + result.order(ByteOrder.LITTLE_ENDIAN); + result.putInt(MAGIC); + result.putInt(CURRENT_VERSION); + result.putInt(encodedLineage.length); + result.put(encodedLineage); + result.flip(); + return result; + } + + public byte[] encodeSigningCertificateLineage() { + return V3SigningCertificateLineage.encodeSigningCertificateLineage(mSigningLineage); + } + + public List<DefaultApkSignerEngine.SignerConfig> sortSignerConfigs( + List<DefaultApkSignerEngine.SignerConfig> signerConfigs) { + if (signerConfigs == null) { + throw new NullPointerException("signerConfigs == null"); + } + + // not the most elegant sort, but we expect signerConfigs to be quite small (1 or 2 signers + // in most cases) and likely already sorted, so not worth the overhead of doing anything + // fancier + List<DefaultApkSignerEngine.SignerConfig> sortedSignerConfigs = + new ArrayList<>(signerConfigs.size()); + for (int i = 0; i < mSigningLineage.size(); i++) { + for (int j = 0; j < signerConfigs.size(); j++) { + DefaultApkSignerEngine.SignerConfig config = signerConfigs.get(j); + if (mSigningLineage.get(i).signingCert.equals(config.getCertificates().get(0))) { + sortedSignerConfigs.add(config); + break; + } + } + } + if (sortedSignerConfigs.size() != signerConfigs.size()) { + throw new IllegalArgumentException("SignerConfigs supplied which are not present in the" + + " SigningCertificateLineage"); + } + return sortedSignerConfigs; + } + + /** + * Returns the SignerCapabilities for the signer in the lineage that matches the provided + * config. + */ + public SignerCapabilities getSignerCapabilities(SignerConfig config) { + if (config == null) { + throw new NullPointerException("config == null"); + } + + X509Certificate cert = config.getCertificate(); + return getSignerCapabilities(cert); + } + + /** + * Returns the SignerCapabilities for the signer in the lineage that matches the provided + * certificate. + */ + public SignerCapabilities getSignerCapabilities(X509Certificate cert) { + if (cert == null) { + throw new NullPointerException("cert == null"); + } + + for (int i = 0; i < mSigningLineage.size(); i++) { + SigningCertificateNode lineageNode = mSigningLineage.get(i); + if (lineageNode.signingCert.equals(cert)) { + int flags = lineageNode.flags; + return new SignerCapabilities.Builder(flags).build(); + } + } + + // the provided signer certificate was not found in the lineage + throw new IllegalArgumentException("Certificate (" + cert.getSubjectDN() + + ") not found in the SigningCertificateLineage"); + } + + /** + * Updates the SignerCapabilities for the signer in the lineage that matches the provided + * config. Only those capabilities that have been modified through the setXX methods will be + * updated for the signer to prevent unset default values from being applied. + */ + public void updateSignerCapabilities(SignerConfig config, SignerCapabilities capabilities) { + if (config == null) { + throw new NullPointerException("config == null"); + } + updateSignerCapabilities(config.getCertificate(), capabilities); + } + + /** + * Updates the {@code capabilities} for the signer with the provided {@code certificate} in the + * lineage. Only those capabilities that have been modified through the setXX methods will be + * updated for the signer to prevent unset default values from being applied. + */ + public void updateSignerCapabilities(X509Certificate certificate, + SignerCapabilities capabilities) { + if (certificate == null) { + throw new NullPointerException("config == null"); + } + + for (int i = 0; i < mSigningLineage.size(); i++) { + SigningCertificateNode lineageNode = mSigningLineage.get(i); + if (lineageNode.signingCert.equals(certificate)) { + int flags = lineageNode.flags; + SignerCapabilities newCapabilities = new SignerCapabilities.Builder( + flags).setCallerConfiguredCapabilities(capabilities).build(); + lineageNode.flags = newCapabilities.getFlags(); + return; + } + } + + // the provided signer config was not found in the lineage + throw new IllegalArgumentException("Certificate (" + certificate.getSubjectDN() + + ") not found in the SigningCertificateLineage"); + } + + /** + * Returns a list containing all of the certificates in the lineage. + */ + public List<X509Certificate> getCertificatesInLineage() { + List<X509Certificate> certs = new ArrayList<>(); + for (int i = 0; i < mSigningLineage.size(); i++) { + X509Certificate cert = mSigningLineage.get(i).signingCert; + certs.add(cert); + } + return certs; + } + + /** + * Returns {@code true} if the specified config is in the lineage. + */ + public boolean isSignerInLineage(SignerConfig config) { + if (config == null) { + throw new NullPointerException("config == null"); + } + + X509Certificate cert = config.getCertificate(); + return isCertificateInLineage(cert); + } + + /** + * Returns {@code true} if the specified certificate is in the lineage. + */ + public boolean isCertificateInLineage(X509Certificate cert) { + if (cert == null) { + throw new NullPointerException("cert == null"); + } + + for (int i = 0; i < mSigningLineage.size(); i++) { + if (mSigningLineage.get(i).signingCert.equals(cert)) { + return true; + } + } + return false; + } + + /** + * Returns whether the provided {@code cert} is the latest signing certificate in the lineage. + * + * <p>This method will only compare the provided {@code cert} against the latest signing + * certificate in the lineage; if a certificate that is not in the lineage is provided, this + * method will return false. + */ + public boolean isCertificateLatestInLineage(X509Certificate cert) { + if (cert == null) { + throw new NullPointerException("cert == null"); + } + + return mSigningLineage.get(mSigningLineage.size() - 1).signingCert.equals(cert); + } + + private static int calculateDefaultFlags() { + return PAST_CERT_INSTALLED_DATA | PAST_CERT_PERMISSION + | PAST_CERT_SHARED_USER_ID | PAST_CERT_AUTH; + } + + /** + * Returns a new SigningCertificateLineage which terminates at the node corresponding to the + * given certificate. This is useful in the event of rotating to a new signing algorithm that + * is only supported on some platform versions. It enables a v3 signature to be generated using + * this signing certificate and the shortened proof-of-rotation record from this sub lineage in + * conjunction with the appropriate SDK version values. + * + * @param x509Certificate the signing certificate for which to search + * @return A new SigningCertificateLineage if the given certificate is present. + * + * @throws IllegalArgumentException if the provided certificate is not in the lineage. + */ + public SigningCertificateLineage getSubLineage(X509Certificate x509Certificate) { + if (x509Certificate == null) { + throw new NullPointerException("x509Certificate == null"); + } + for (int i = 0; i < mSigningLineage.size(); i++) { + if (mSigningLineage.get(i).signingCert.equals(x509Certificate)) { + return new SigningCertificateLineage( + mMinSdkVersion, new ArrayList<>(mSigningLineage.subList(0, i + 1))); + } + } + + // looks like we didn't find the cert, + throw new IllegalArgumentException("Certificate not found in SigningCertificateLineage"); + } + + /** + * Consolidates all of the lineages found in an APK into one lineage. In so doing, it also + * checks that all of the lineages are contained in one common lineage. + * + * An APK may contain multiple lineages, one for each signer, which correspond to different + * supported platform versions. In this event, the lineage(s) from the earlier platform + * version(s) should be present in the most recent, either directly or via a sublineage + * that would allow the earlier lineages to merge with the most recent. + * + * <note> This does not verify that the largest lineage corresponds to the most recent supported + * platform version. That check is performed during v3 verification. </note> + */ + public static SigningCertificateLineage consolidateLineages( + List<SigningCertificateLineage> lineages) { + if (lineages == null || lineages.isEmpty()) { + return null; + } + SigningCertificateLineage consolidatedLineage = lineages.get(0); + for (int i = 1; i < lineages.size(); i++) { + consolidatedLineage = consolidatedLineage.mergeLineageWith(lineages.get(i)); + } + return consolidatedLineage; + } + + /** + * Merges this lineage with the provided {@code otherLineage}. + * + * <p>The merged lineage does not currently handle merging capabilities of common signers and + * should only be used to determine the full signing history of a collection of lineages. + */ + public SigningCertificateLineage mergeLineageWith(SigningCertificateLineage otherLineage) { + // Determine the ancestor and descendant lineages; if the original signer is in the other + // lineage, then it is considered a descendant. + SigningCertificateLineage ancestorLineage; + SigningCertificateLineage descendantLineage; + X509Certificate signerCert = mSigningLineage.get(0).signingCert; + if (otherLineage.isCertificateInLineage(signerCert)) { + descendantLineage = this; + ancestorLineage = otherLineage; + } else { + descendantLineage = otherLineage; + ancestorLineage = this; + } + + int ancestorIndex = 0; + int descendantIndex = 0; + SigningCertificateNode ancestorNode; + SigningCertificateNode descendantNode = descendantLineage.mSigningLineage.get( + descendantIndex++); + List<SigningCertificateNode> mergedLineage = new ArrayList<>(); + // Iterate through the ancestor lineage and add the current node to the resulting lineage + // until the first node of the descendant is found. + while (ancestorIndex < ancestorLineage.size()) { + ancestorNode = ancestorLineage.mSigningLineage.get(ancestorIndex++); + if (ancestorNode.signingCert.equals(descendantNode.signingCert)) { + break; + } + mergedLineage.add(ancestorNode); + } + // If all of the nodes in the ancestor lineage have been added to the merged lineage, then + // there is no overlap between this and the provided lineage. + if (ancestorIndex == mergedLineage.size()) { + throw new IllegalArgumentException( + "The provided lineage is not a descendant or an ancestor of this lineage"); + } + // The descendant lineage's first node was in the ancestor's lineage above; add it to the + // merged lineage. + mergedLineage.add(descendantNode); + while (ancestorIndex < ancestorLineage.size() + && descendantIndex < descendantLineage.size()) { + ancestorNode = ancestorLineage.mSigningLineage.get(ancestorIndex++); + descendantNode = descendantLineage.mSigningLineage.get(descendantIndex++); + if (!ancestorNode.signingCert.equals(descendantNode.signingCert)) { + throw new IllegalArgumentException( + "The provided lineage diverges from this lineage"); + } + mergedLineage.add(descendantNode); + } + // At this point, one or both of the lineages have been exhausted and all signers to this + // point were a match between the two lineages; add any remaining elements from either + // lineage to the merged lineage. + while (ancestorIndex < ancestorLineage.size()) { + mergedLineage.add(ancestorLineage.mSigningLineage.get(ancestorIndex++)); + } + while (descendantIndex < descendantLineage.size()) { + mergedLineage.add(descendantLineage.mSigningLineage.get(descendantIndex++)); + } + return new SigningCertificateLineage(Math.min(mMinSdkVersion, otherLineage.mMinSdkVersion), + mergedLineage); + } + + /** + * Checks whether given lineages are compatible. Returns {@code true} if an installed APK with + * the oldLineage could be updated with an APK with the newLineage. + */ + public static boolean checkLineagesCompatibility( + SigningCertificateLineage oldLineage, SigningCertificateLineage newLineage) { + + final ArrayList<X509Certificate> oldCertificates = oldLineage == null ? + new ArrayList<X509Certificate>() + : new ArrayList(oldLineage.getCertificatesInLineage()); + final ArrayList<X509Certificate> newCertificates = newLineage == null ? + new ArrayList<X509Certificate>() + : new ArrayList(newLineage.getCertificatesInLineage()); + + if (oldCertificates.isEmpty()) { + return true; + } + if (newCertificates.isEmpty()) { + return false; + } + + // Both lineages contain exactly the same certificates or the new lineage extends + // the old one. The capabilities of particular certificates may have changed though but it + // does not matter in terms of current compatibility. + if (newCertificates.size() >= oldCertificates.size() + && newCertificates.subList(0, oldCertificates.size()).equals(oldCertificates)) { + return true; + } + + ArrayList<X509Certificate> newCertificatesArray = new ArrayList(newCertificates); + ArrayList<X509Certificate> oldCertificatesArray = new ArrayList(oldCertificates); + + int lastOldCertIndexInNew = newCertificatesArray.lastIndexOf( + oldCertificatesArray.get(oldCertificatesArray.size()-1)); + + // The new lineage trims some nodes from the beginning of the old lineage and possibly + // extends it at the end. The new lineage must contain the old signing certificate and + // the nodes up until the node with signing certificate must be in the same order. + // Good example 1: + // old: A -> B -> C + // new: B -> C -> D + // Good example 2: + // old: A -> B -> C + // new: C + // Bad example 1: + // old: A -> B -> C + // new: A -> C + // Bad example 1: + // old: A -> B + // new: C -> B + if (lastOldCertIndexInNew >= 0) { + return newCertificatesArray.subList(0, lastOldCertIndexInNew+1).equals( + oldCertificatesArray.subList( + oldCertificates.size()-1-lastOldCertIndexInNew, + oldCertificatesArray.size())); + } + + + // The new lineage can be shorter than the old one only if the last certificate of the new + // lineage exists in the old lineage and has a rollback capability there. + // Good example: + // old: A -> B_withRollbackCapability -> C + // new: A -> B + // Bad example 1: + // old: A -> B -> C + // new: A -> B + // Bad example 2: + // old: A -> B_withRollbackCapability -> C + // new: A -> B -> D + return oldCertificates.subList(0, newCertificates.size()).equals(newCertificates) + && oldLineage.getSignerCapabilities( + oldCertificates.get(newCertificates.size()-1)).hasRollback(); + } + + /** + * Representation of the capabilities the APK would like to grant to its old signing + * certificates. The {@code SigningCertificateLineage} provides two conceptual data structures. + * 1) proof of rotation - Evidence that other parties can trust an APK's current signing + * certificate if they trust an older one in this lineage + * 2) self-trust - certain capabilities may have been granted by an APK to other parties based + * on its own signing certificate. When it changes its signing certificate it may want to + * allow the other parties to retain those capabilities. + * {@code SignerCapabilties} provides a representation of the second structure. + * + * <p>Use {@link Builder} to obtain configuration instances. + */ + public static class SignerCapabilities { + private final int mFlags; + + private final int mCallerConfiguredFlags; + + private SignerCapabilities(int flags) { + this(flags, 0); + } + + private SignerCapabilities(int flags, int callerConfiguredFlags) { + mFlags = flags; + mCallerConfiguredFlags = callerConfiguredFlags; + } + + private int getFlags() { + return mFlags; + } + + /** + * Returns {@code true} if the capabilities of this object match those of the provided + * object. + */ + @Override + public boolean equals(Object other) { + if (this == other) return true; + if (!(other instanceof SignerCapabilities)) return false; + + return this.mFlags == ((SignerCapabilities) other).mFlags; + } + + @Override + public int hashCode() { + return 31 * mFlags; + } + + /** + * Returns {@code true} if this object has the installed data capability. + */ + public boolean hasInstalledData() { + return (mFlags & PAST_CERT_INSTALLED_DATA) != 0; + } + + /** + * Returns {@code true} if this object has the shared UID capability. + */ + public boolean hasSharedUid() { + return (mFlags & PAST_CERT_SHARED_USER_ID) != 0; + } + + /** + * Returns {@code true} if this object has the permission capability. + */ + public boolean hasPermission() { + return (mFlags & PAST_CERT_PERMISSION) != 0; + } + + /** + * Returns {@code true} if this object has the rollback capability. + */ + public boolean hasRollback() { + return (mFlags & PAST_CERT_ROLLBACK) != 0; + } + + /** + * Returns {@code true} if this object has the auth capability. + */ + public boolean hasAuth() { + return (mFlags & PAST_CERT_AUTH) != 0; + } + + /** + * Builder of {@link SignerCapabilities} instances. + */ + public static class Builder { + private int mFlags; + + private int mCallerConfiguredFlags; + + /** + * Constructs a new {@code Builder}. + */ + public Builder() { + mFlags = calculateDefaultFlags(); + } + + /** + * Constructs a new {@code Builder} with the initial capabilities set to the provided + * flags. + */ + public Builder(int flags) { + mFlags = flags; + } + + /** + * Set the {@code PAST_CERT_INSTALLED_DATA} flag in this capabilities object. This flag + * is used by the platform to determine if installed data associated with previous + * signing certificate should be trusted. In particular, this capability is required to + * perform signing certificate rotation during an upgrade on-device. Without it, the + * platform will not permit the app data from the old signing certificate to + * propagate to the new version. Typically, this flag should be set to enable signing + * certificate rotation, and may be unset later when the app developer is satisfied that + * their install base is as migrated as it will be. + */ + public Builder setInstalledData(boolean enabled) { + mCallerConfiguredFlags |= PAST_CERT_INSTALLED_DATA; + if (enabled) { + mFlags |= PAST_CERT_INSTALLED_DATA; + } else { + mFlags &= ~PAST_CERT_INSTALLED_DATA; + } + return this; + } + + /** + * Set the {@code PAST_CERT_SHARED_USER_ID} flag in this capabilities object. This flag + * is used by the platform to determine if this app is willing to be sharedUid with + * other apps which are still signed with the associated signing certificate. This is + * useful in situations where sharedUserId apps would like to change their signing + * certificate, but can't guarantee the order of updates to those apps. + */ + public Builder setSharedUid(boolean enabled) { + mCallerConfiguredFlags |= PAST_CERT_SHARED_USER_ID; + if (enabled) { + mFlags |= PAST_CERT_SHARED_USER_ID; + } else { + mFlags &= ~PAST_CERT_SHARED_USER_ID; + } + return this; + } + + /** + * Set the {@code PAST_CERT_PERMISSION} flag in this capabilities object. This flag + * is used by the platform to determine if this app is willing to grant SIGNATURE + * permissions to apps signed with the associated signing certificate. Without this + * capability, an application signed with the older certificate will not be granted the + * SIGNATURE permissions defined by this app. In addition, if multiple apps define the + * same SIGNATURE permission, the second one the platform sees will not be installable + * if this capability is not set and the signing certificates differ. + */ + public Builder setPermission(boolean enabled) { + mCallerConfiguredFlags |= PAST_CERT_PERMISSION; + if (enabled) { + mFlags |= PAST_CERT_PERMISSION; + } else { + mFlags &= ~PAST_CERT_PERMISSION; + } + return this; + } + + /** + * Set the {@code PAST_CERT_ROLLBACK} flag in this capabilities object. This flag + * is used by the platform to determine if this app is willing to upgrade to a new + * version that is signed by one of its past signing certificates. + * + * <note> WARNING: this effectively removes any benefit of signing certificate changes, + * since a compromised key could retake control of an app even after change, and should + * only be used if there is a problem encountered when trying to ditch an older cert + * </note> + */ + public Builder setRollback(boolean enabled) { + mCallerConfiguredFlags |= PAST_CERT_ROLLBACK; + if (enabled) { + mFlags |= PAST_CERT_ROLLBACK; + } else { + mFlags &= ~PAST_CERT_ROLLBACK; + } + return this; + } + + /** + * Set the {@code PAST_CERT_AUTH} flag in this capabilities object. This flag + * is used by the platform to determine whether or not privileged access based on + * authenticator module signing certificates should be granted. + */ + public Builder setAuth(boolean enabled) { + mCallerConfiguredFlags |= PAST_CERT_AUTH; + if (enabled) { + mFlags |= PAST_CERT_AUTH; + } else { + mFlags &= ~PAST_CERT_AUTH; + } + return this; + } + + /** + * Applies the capabilities that were explicitly set in the provided capabilities object + * to this builder. Any values that were not set will not be applied to this builder + * to prevent unintentinoally setting a capability back to a default value. + */ + public Builder setCallerConfiguredCapabilities(SignerCapabilities capabilities) { + // The mCallerConfiguredFlags should have a bit set for each capability that was + // set by a caller. If a capability was explicitly set then the corresponding bit + // in mCallerConfiguredFlags should be set. This allows the provided capabilities + // to take effect for those set by the caller while those that were not set will + // be cleared by the bitwise and and the initial value for the builder will remain. + mFlags = (mFlags & ~capabilities.mCallerConfiguredFlags) | + (capabilities.mFlags & capabilities.mCallerConfiguredFlags); + return this; + } + + /** + * Returns a new {@code SignerConfig} instance configured based on the configuration of + * this builder. + */ + public SignerCapabilities build() { + return new SignerCapabilities(mFlags, mCallerConfiguredFlags); + } + } + } + + /** + * Configuration of a signer. Used to add a new entry to the {@link SigningCertificateLineage} + * + * <p>Use {@link Builder} to obtain configuration instances. + */ + public static class SignerConfig { + private final PrivateKey mPrivateKey; + private final X509Certificate mCertificate; + + private SignerConfig( + PrivateKey privateKey, + X509Certificate certificate) { + mPrivateKey = privateKey; + mCertificate = certificate; + } + + /** + * Returns the signing key of this signer. + */ + public PrivateKey getPrivateKey() { + return mPrivateKey; + } + + /** + * Returns the certificate(s) of this signer. The first certificate's public key corresponds + * to this signer's private key. + */ + public X509Certificate getCertificate() { + return mCertificate; + } + + /** + * Builder of {@link SignerConfig} instances. + */ + public static class Builder { + private final PrivateKey mPrivateKey; + private final X509Certificate mCertificate; + + /** + * Constructs a new {@code Builder}. + * + * @param privateKey signing key + * @param certificate the X.509 certificate with a subject public key of the + * {@code privateKey}. + */ + public Builder( + PrivateKey privateKey, + X509Certificate certificate) { + mPrivateKey = privateKey; + mCertificate = certificate; + } + + /** + * Returns a new {@code SignerConfig} instance configured based on the configuration of + * this builder. + */ + public SignerConfig build() { + return new SignerConfig( + mPrivateKey, + mCertificate); + } + } + } + + /** + * Builder of {@link SigningCertificateLineage} instances. + */ + public static class Builder { + private final SignerConfig mOriginalSignerConfig; + private final SignerConfig mNewSignerConfig; + private SignerCapabilities mOriginalCapabilities; + private SignerCapabilities mNewCapabilities; + private int mMinSdkVersion; + /** + * Constructs a new {@code Builder}. + * + * @param originalSignerConfig first signer in this lineage, parent of the next + * @param newSignerConfig new signer in the lineage; the new signing key that the APK will + * use + */ + public Builder( + SignerConfig originalSignerConfig, + SignerConfig newSignerConfig) { + if (originalSignerConfig == null || newSignerConfig == null) { + throw new NullPointerException("Can't pass null SignerConfigs when constructing a " + + "new SigningCertificateLineage"); + } + mOriginalSignerConfig = originalSignerConfig; + mNewSignerConfig = newSignerConfig; + } + + /** + * Constructs a new {@code Builder} that is intended to create a {@code + * SigningCertificateLineage} with a single signer in the signing history. + * + * @param originalSignerConfig first signer in this lineage + */ + public Builder(SignerConfig originalSignerConfig) { + if (originalSignerConfig == null) { + throw new NullPointerException("Can't pass null SignerConfigs when constructing a " + + "new SigningCertificateLineage"); + } + mOriginalSignerConfig = originalSignerConfig; + mNewSignerConfig = null; + } + + /** + * Sets the minimum Android platform version (API Level) on which this lineage is expected + * to validate. It is possible that newer signers in the lineage may not be recognized on + * the given platform, but as long as an older signer is, the lineage can still be used to + * sign an APK for the given platform. + * + * <note> By default, this value is set to the value for the + * P release, since this structure was created for that release, and will also be set to + * that value if a smaller one is specified. </note> + */ + public Builder setMinSdkVersion(int minSdkVersion) { + mMinSdkVersion = minSdkVersion; + return this; + } + + /** + * Sets capabilities to give {@code mOriginalSignerConfig}. These capabilities allow an + * older signing certificate to still be used in some situations on the platform even though + * the APK is now being signed by a newer signing certificate. + */ + public Builder setOriginalCapabilities(SignerCapabilities signerCapabilities) { + if (signerCapabilities == null) { + throw new NullPointerException("signerCapabilities == null"); + } + mOriginalCapabilities = signerCapabilities; + return this; + } + + /** + * Sets capabilities to give {@code mNewSignerConfig}. These capabilities allow an + * older signing certificate to still be used in some situations on the platform even though + * the APK is now being signed by a newer signing certificate. By default, the new signer + * will have all capabilities, so when first switching to a new signing certificate, these + * capabilities have no effect, but they will act as the default level of trust when moving + * to a new signing certificate. + */ + public Builder setNewCapabilities(SignerCapabilities signerCapabilities) { + if (signerCapabilities == null) { + throw new NullPointerException("signerCapabilities == null"); + } + mNewCapabilities = signerCapabilities; + return this; + } + + public SigningCertificateLineage build() + throws CertificateEncodingException, InvalidKeyException, NoSuchAlgorithmException, + SignatureException { + if (mMinSdkVersion < AndroidSdkVersion.P) { + mMinSdkVersion = AndroidSdkVersion.P; + } + + if (mOriginalCapabilities == null) { + mOriginalCapabilities = new SignerCapabilities.Builder().build(); + } + + if (mNewSignerConfig == null) { + return createSigningLineage(mMinSdkVersion, mOriginalSignerConfig, + mOriginalCapabilities); + } + + if (mNewCapabilities == null) { + mNewCapabilities = new SignerCapabilities.Builder().build(); + } + + return createSigningLineage( + mMinSdkVersion, mOriginalSignerConfig, mOriginalCapabilities, + mNewSignerConfig, mNewCapabilities); + } + } +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/SourceStampVerifier.java b/platform/android/java/editor/src/main/java/com/android/apksig/SourceStampVerifier.java new file mode 100644 index 0000000000..98da68ea8f --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/SourceStampVerifier.java @@ -0,0 +1,911 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig; + +import static com.android.apksig.Constants.VERSION_APK_SIGNATURE_SCHEME_V2; +import static com.android.apksig.Constants.VERSION_APK_SIGNATURE_SCHEME_V3; +import static com.android.apksig.Constants.VERSION_JAR_SIGNATURE_SCHEME; +import static com.android.apksig.apk.ApkUtilsLite.computeSha256DigestBytes; +import static com.android.apksig.internal.apk.stamp.SourceStampConstants.SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME; +import static com.android.apksig.internal.apk.v1.V1SchemeConstants.MANIFEST_ENTRY_NAME; + +import com.android.apksig.apk.ApkFormatException; +import com.android.apksig.apk.ApkUtilsLite; +import com.android.apksig.internal.apk.ApkSigResult; +import com.android.apksig.internal.apk.ApkSignerInfo; +import com.android.apksig.internal.apk.ApkSigningBlockUtilsLite; +import com.android.apksig.internal.apk.ContentDigestAlgorithm; +import com.android.apksig.internal.apk.SignatureAlgorithm; +import com.android.apksig.internal.apk.SignatureInfo; +import com.android.apksig.internal.apk.SignatureNotFoundException; +import com.android.apksig.internal.apk.stamp.SourceStampConstants; +import com.android.apksig.internal.apk.stamp.V2SourceStampVerifier; +import com.android.apksig.internal.apk.v2.V2SchemeConstants; +import com.android.apksig.internal.apk.v3.V3SchemeConstants; +import com.android.apksig.internal.util.AndroidSdkVersion; +import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate; +import com.android.apksig.internal.zip.CentralDirectoryRecord; +import com.android.apksig.internal.zip.LocalFileRecord; +import com.android.apksig.internal.zip.ZipUtils; +import com.android.apksig.util.DataSource; +import com.android.apksig.util.DataSources; +import com.android.apksig.zip.ZipFormatException; +import com.android.apksig.zip.ZipSections; + +import java.io.ByteArrayInputStream; +import java.io.Closeable; +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.security.NoSuchAlgorithmException; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * APK source stamp verifier intended only to verify the validity of the stamp signature. + * + * <p>Note, this verifier does not validate the signatures of the jar signing / APK signature blocks + * when obtaining the digests for verification. This verifier should only be used in cases where + * another mechanism has already been used to verify the APK signatures. + */ +public class SourceStampVerifier { + private final File mApkFile; + private final DataSource mApkDataSource; + + private final int mMinSdkVersion; + private final int mMaxSdkVersion; + + private SourceStampVerifier( + File apkFile, + DataSource apkDataSource, + int minSdkVersion, + int maxSdkVersion) { + mApkFile = apkFile; + mApkDataSource = apkDataSource; + mMinSdkVersion = minSdkVersion; + mMaxSdkVersion = maxSdkVersion; + } + + /** + * Verifies the APK's source stamp signature and returns the result of the verification. + * + * <p>The APK's source stamp can be considered verified if the result's {@link + * Result#isVerified()} returns {@code true}. If source stamp verification fails all of the + * resulting errors can be obtained from {@link Result#getAllErrors()}, or individual errors + * can be obtained as follows: + * <ul> + * <li>Obtain the generic errors via {@link Result#getErrors()} + * <li>Obtain the V2 signers via {@link Result#getV2SchemeSigners()}, then for each signer + * query for any errors with {@link Result.SignerInfo#getErrors()} + * <li>Obtain the V3 signers via {@link Result#getV3SchemeSigners()}, then for each signer + * query for any errors with {@link Result.SignerInfo#getErrors()} + * <li>Obtain the source stamp signer via {@link Result#getSourceStampInfo()}, then query + * for any stamp errors with {@link Result.SourceStampInfo#getErrors()} + * </ul> + */ + public SourceStampVerifier.Result verifySourceStamp() { + return verifySourceStamp(null); + } + + /** + * Verifies the APK's source stamp signature, including verification that the SHA-256 digest of + * the stamp signing certificate matches the {@code expectedCertDigest}, and returns the result + * of the verification. + * + * <p>A value of {@code null} for the {@code expectedCertDigest} will verify the source stamp, + * if present, without verifying the actual source stamp certificate used to sign the source + * stamp. This can be used to verify an APK contains a properly signed source stamp without + * verifying a particular signer. + * + * @see #verifySourceStamp() + */ + public SourceStampVerifier.Result verifySourceStamp(String expectedCertDigest) { + Closeable in = null; + try { + DataSource apk; + if (mApkDataSource != null) { + apk = mApkDataSource; + } else if (mApkFile != null) { + RandomAccessFile f = new RandomAccessFile(mApkFile, "r"); + in = f; + apk = DataSources.asDataSource(f, 0, f.length()); + } else { + throw new IllegalStateException("APK not provided"); + } + return verifySourceStamp(apk, expectedCertDigest); + } catch (IOException e) { + Result result = new Result(); + result.addVerificationError(ApkVerificationIssue.UNEXPECTED_EXCEPTION, e); + return result; + } finally { + if (in != null) { + try { + in.close(); + } catch (IOException ignored) { + } + } + } + } + + /** + * Verifies the provided {@code apk}'s source stamp signature, including verification of the + * SHA-256 digest of the stamp signing certificate matches the {@code expectedCertDigest}, and + * returns the result of the verification. + * + * @see #verifySourceStamp(String) + */ + private SourceStampVerifier.Result verifySourceStamp(DataSource apk, + String expectedCertDigest) { + Result result = new Result(); + try { + ZipSections zipSections = ApkUtilsLite.findZipSections(apk); + // Attempt to obtain the source stamp's certificate digest from the APK. + List<CentralDirectoryRecord> cdRecords = + ZipUtils.parseZipCentralDirectory(apk, zipSections); + CentralDirectoryRecord sourceStampCdRecord = null; + for (CentralDirectoryRecord cdRecord : cdRecords) { + if (SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME.equals(cdRecord.getName())) { + sourceStampCdRecord = cdRecord; + break; + } + } + + // If the source stamp's certificate digest is not available within the APK then the + // source stamp cannot be verified; check if a source stamp signing block is in the + // APK's signature block to determine the appropriate status to return. + if (sourceStampCdRecord == null) { + boolean stampSigningBlockFound; + try { + ApkSigningBlockUtilsLite.findSignature(apk, zipSections, + SourceStampConstants.V2_SOURCE_STAMP_BLOCK_ID); + stampSigningBlockFound = true; + } catch (SignatureNotFoundException e) { + stampSigningBlockFound = false; + } + result.addVerificationError(stampSigningBlockFound + ? ApkVerificationIssue.SOURCE_STAMP_SIGNATURE_BLOCK_WITHOUT_CERT_DIGEST + : ApkVerificationIssue.SOURCE_STAMP_CERT_DIGEST_AND_SIG_BLOCK_MISSING); + return result; + } + + // Verify that the contents of the source stamp certificate digest match the expected + // value, if provided. + byte[] sourceStampCertificateDigest = + LocalFileRecord.getUncompressedData( + apk, + sourceStampCdRecord, + zipSections.getZipCentralDirectoryOffset()); + if (expectedCertDigest != null) { + String actualCertDigest = ApkSigningBlockUtilsLite.toHex( + sourceStampCertificateDigest); + if (!expectedCertDigest.equalsIgnoreCase(actualCertDigest)) { + result.addVerificationError( + ApkVerificationIssue.SOURCE_STAMP_EXPECTED_DIGEST_MISMATCH, + actualCertDigest, expectedCertDigest); + return result; + } + } + + Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeApkContentDigests = + new HashMap<>(); + if (mMaxSdkVersion >= AndroidSdkVersion.P) { + SignatureInfo signatureInfo; + try { + signatureInfo = ApkSigningBlockUtilsLite.findSignature(apk, zipSections, + V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID); + } catch (SignatureNotFoundException e) { + signatureInfo = null; + } + if (signatureInfo != null) { + Map<ContentDigestAlgorithm, byte[]> apkContentDigests = new EnumMap<>( + ContentDigestAlgorithm.class); + parseSigners(signatureInfo.signatureBlock, VERSION_APK_SIGNATURE_SCHEME_V3, + apkContentDigests, result); + signatureSchemeApkContentDigests.put( + VERSION_APK_SIGNATURE_SCHEME_V3, apkContentDigests); + } + } + + if (mMaxSdkVersion >= AndroidSdkVersion.N && (mMinSdkVersion < AndroidSdkVersion.P || + signatureSchemeApkContentDigests.isEmpty())) { + SignatureInfo signatureInfo; + try { + signatureInfo = ApkSigningBlockUtilsLite.findSignature(apk, zipSections, + V2SchemeConstants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID); + } catch (SignatureNotFoundException e) { + signatureInfo = null; + } + if (signatureInfo != null) { + Map<ContentDigestAlgorithm, byte[]> apkContentDigests = new EnumMap<>( + ContentDigestAlgorithm.class); + parseSigners(signatureInfo.signatureBlock, VERSION_APK_SIGNATURE_SCHEME_V2, + apkContentDigests, result); + signatureSchemeApkContentDigests.put( + VERSION_APK_SIGNATURE_SCHEME_V2, apkContentDigests); + } + } + + if (mMinSdkVersion < AndroidSdkVersion.N + || signatureSchemeApkContentDigests.isEmpty()) { + Map<ContentDigestAlgorithm, byte[]> apkContentDigests = + getApkContentDigestFromV1SigningScheme(cdRecords, apk, zipSections, result); + signatureSchemeApkContentDigests.put(VERSION_JAR_SIGNATURE_SCHEME, + apkContentDigests); + } + + ApkSigResult sourceStampResult = + V2SourceStampVerifier.verify( + apk, + zipSections, + sourceStampCertificateDigest, + signatureSchemeApkContentDigests, + mMinSdkVersion, + mMaxSdkVersion); + result.mergeFrom(sourceStampResult); + return result; + } catch (ApkFormatException | IOException | ZipFormatException e) { + result.addVerificationError(ApkVerificationIssue.MALFORMED_APK, e); + } catch (NoSuchAlgorithmException e) { + result.addVerificationError(ApkVerificationIssue.UNEXPECTED_EXCEPTION, e); + } catch (SignatureNotFoundException e) { + result.addVerificationError(ApkVerificationIssue.SOURCE_STAMP_SIG_MISSING); + } + return result; + } + + /** + * Parses each signer in the provided APK V2 / V3 signature block and populates corresponding + * {@code SignerInfo} of the provided {@code result} and their {@code apkContentDigests}. + * + * <p>This method adds one or more errors to the {@code result} if a verification error is + * expected to be encountered on an Android platform version in the + * {@code [minSdkVersion, maxSdkVersion]} range. + */ + public static void parseSigners( + ByteBuffer apkSignatureSchemeBlock, + int apkSigSchemeVersion, + Map<ContentDigestAlgorithm, byte[]> apkContentDigests, + Result result) { + boolean isV2Block = apkSigSchemeVersion == VERSION_APK_SIGNATURE_SCHEME_V2; + // Both the V2 and V3 signature blocks contain the following: + // * length-prefixed sequence of length-prefixed signers + ByteBuffer signers; + try { + signers = ApkSigningBlockUtilsLite.getLengthPrefixedSlice(apkSignatureSchemeBlock); + } catch (ApkFormatException e) { + result.addVerificationWarning(isV2Block ? ApkVerificationIssue.V2_SIG_MALFORMED_SIGNERS + : ApkVerificationIssue.V3_SIG_MALFORMED_SIGNERS); + return; + } + if (!signers.hasRemaining()) { + result.addVerificationWarning(isV2Block ? ApkVerificationIssue.V2_SIG_NO_SIGNERS + : ApkVerificationIssue.V3_SIG_NO_SIGNERS); + return; + } + + CertificateFactory certFactory; + try { + certFactory = CertificateFactory.getInstance("X.509"); + } catch (CertificateException e) { + throw new RuntimeException("Failed to obtain X.509 CertificateFactory", e); + } + while (signers.hasRemaining()) { + Result.SignerInfo signerInfo = new Result.SignerInfo(); + if (isV2Block) { + result.addV2Signer(signerInfo); + } else { + result.addV3Signer(signerInfo); + } + try { + ByteBuffer signer = ApkSigningBlockUtilsLite.getLengthPrefixedSlice(signers); + parseSigner( + signer, + apkSigSchemeVersion, + certFactory, + apkContentDigests, + signerInfo); + } catch (ApkFormatException | BufferUnderflowException e) { + signerInfo.addVerificationWarning( + isV2Block ? ApkVerificationIssue.V2_SIG_MALFORMED_SIGNER + : ApkVerificationIssue.V3_SIG_MALFORMED_SIGNER); + return; + } + } + } + + /** + * Parses the provided signer block and populates the {@code result}. + * + * <p>This verifies signatures over {@code signed-data} contained in this block but does not + * verify the integrity of the rest of the APK. To facilitate APK integrity verification, this + * method adds the {@code contentDigestsToVerify}. These digests can then be used to verify the + * integrity of the APK. + * + * <p>This method adds one or more errors to the {@code result} if a verification error is + * expected to be encountered on an Android platform version in the + * {@code [minSdkVersion, maxSdkVersion]} range. + */ + private static void parseSigner( + ByteBuffer signerBlock, + int apkSigSchemeVersion, + CertificateFactory certFactory, + Map<ContentDigestAlgorithm, byte[]> apkContentDigests, + Result.SignerInfo signerInfo) + throws ApkFormatException { + boolean isV2Signer = apkSigSchemeVersion == VERSION_APK_SIGNATURE_SCHEME_V2; + // Both the V2 and V3 signer blocks contain the following: + // * length-prefixed signed data + // * length-prefixed sequence of length-prefixed digests: + // * uint32: signature algorithm ID + // * length-prefixed bytes: digest of contents + // * length-prefixed sequence of certificates: + // * length-prefixed bytes: X.509 certificate (ASN.1 DER encoded). + ByteBuffer signedData = ApkSigningBlockUtilsLite.getLengthPrefixedSlice(signerBlock); + ByteBuffer digests = ApkSigningBlockUtilsLite.getLengthPrefixedSlice(signedData); + ByteBuffer certificates = ApkSigningBlockUtilsLite.getLengthPrefixedSlice(signedData); + + // Parse the digests block + while (digests.hasRemaining()) { + try { + ByteBuffer digest = ApkSigningBlockUtilsLite.getLengthPrefixedSlice(digests); + int sigAlgorithmId = digest.getInt(); + byte[] digestBytes = ApkSigningBlockUtilsLite.readLengthPrefixedByteArray(digest); + SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.findById(sigAlgorithmId); + if (signatureAlgorithm == null) { + continue; + } + apkContentDigests.put(signatureAlgorithm.getContentDigestAlgorithm(), digestBytes); + } catch (ApkFormatException | BufferUnderflowException e) { + signerInfo.addVerificationWarning( + isV2Signer ? ApkVerificationIssue.V2_SIG_MALFORMED_DIGEST + : ApkVerificationIssue.V3_SIG_MALFORMED_DIGEST); + return; + } + } + + // Parse the certificates block + if (certificates.hasRemaining()) { + byte[] encodedCert = ApkSigningBlockUtilsLite.readLengthPrefixedByteArray(certificates); + X509Certificate certificate; + try { + certificate = (X509Certificate) certFactory.generateCertificate( + new ByteArrayInputStream(encodedCert)); + } catch (CertificateException e) { + signerInfo.addVerificationWarning( + isV2Signer ? ApkVerificationIssue.V2_SIG_MALFORMED_CERTIFICATE + : ApkVerificationIssue.V3_SIG_MALFORMED_CERTIFICATE); + return; + } + // Wrap the cert so that the result's getEncoded returns exactly the original encoded + // form. Without this, getEncoded may return a different form from what was stored in + // the signature. This is because some X509Certificate(Factory) implementations + // re-encode certificates. + certificate = new GuaranteedEncodedFormX509Certificate(certificate, encodedCert); + signerInfo.setSigningCertificate(certificate); + } + + if (signerInfo.getSigningCertificate() == null) { + signerInfo.addVerificationWarning( + isV2Signer ? ApkVerificationIssue.V2_SIG_NO_CERTIFICATES + : ApkVerificationIssue.V3_SIG_NO_CERTIFICATES); + return; + } + } + + /** + * Returns a mapping of the {@link ContentDigestAlgorithm} to the {@code byte[]} digest of the + * V1 / jar signing META-INF/MANIFEST.MF; if this file is not found then an empty {@code Map} is + * returned. + * + * <p>If any errors are encountered while parsing the V1 signers the provided {@code result} + * will be updated to include a warning, but the source stamp verification can still proceed. + */ + private static Map<ContentDigestAlgorithm, byte[]> getApkContentDigestFromV1SigningScheme( + List<CentralDirectoryRecord> cdRecords, + DataSource apk, + ZipSections zipSections, + Result result) + throws IOException, ApkFormatException { + CentralDirectoryRecord manifestCdRecord = null; + List<CentralDirectoryRecord> signatureBlockRecords = new ArrayList<>(1); + Map<ContentDigestAlgorithm, byte[]> v1ContentDigest = new EnumMap<>( + ContentDigestAlgorithm.class); + for (CentralDirectoryRecord cdRecord : cdRecords) { + String cdRecordName = cdRecord.getName(); + if (cdRecordName == null) { + continue; + } + if (manifestCdRecord == null && MANIFEST_ENTRY_NAME.equals(cdRecordName)) { + manifestCdRecord = cdRecord; + continue; + } + if (cdRecordName.startsWith("META-INF/") + && (cdRecordName.endsWith(".RSA") + || cdRecordName.endsWith(".DSA") + || cdRecordName.endsWith(".EC"))) { + signatureBlockRecords.add(cdRecord); + } + } + if (manifestCdRecord == null) { + // No JAR signing manifest file found. For SourceStamp verification, returning an empty + // digest is enough since this would affect the final digest signed by the stamp, and + // thus an empty digest will invalidate that signature. + return v1ContentDigest; + } + if (signatureBlockRecords.isEmpty()) { + result.addVerificationWarning(ApkVerificationIssue.JAR_SIG_NO_SIGNATURES); + } else { + for (CentralDirectoryRecord signatureBlockRecord : signatureBlockRecords) { + try { + CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); + byte[] signatureBlockBytes = LocalFileRecord.getUncompressedData(apk, + signatureBlockRecord, zipSections.getZipCentralDirectoryOffset()); + for (Certificate certificate : certFactory.generateCertificates( + new ByteArrayInputStream(signatureBlockBytes))) { + // If multiple certificates are found within the signature block only the + // first is used as the signer of this block. + if (certificate instanceof X509Certificate) { + Result.SignerInfo signerInfo = new Result.SignerInfo(); + signerInfo.setSigningCertificate((X509Certificate) certificate); + result.addV1Signer(signerInfo); + break; + } + } + } catch (CertificateException e) { + // Log a warning for the parsing exception but still proceed with the stamp + // verification. + result.addVerificationWarning(ApkVerificationIssue.JAR_SIG_PARSE_EXCEPTION, + signatureBlockRecord.getName(), e); + break; + } catch (ZipFormatException e) { + throw new ApkFormatException("Failed to read APK", e); + } + } + } + try { + byte[] manifestBytes = + LocalFileRecord.getUncompressedData( + apk, manifestCdRecord, zipSections.getZipCentralDirectoryOffset()); + v1ContentDigest.put( + ContentDigestAlgorithm.SHA256, computeSha256DigestBytes(manifestBytes)); + return v1ContentDigest; + } catch (ZipFormatException e) { + throw new ApkFormatException("Failed to read APK", e); + } + } + + /** + * Result of verifying the APK's source stamp signature; this signature can only be considered + * verified if {@link #isVerified()} returns true. + */ + public static class Result { + private final List<SignerInfo> mV1SchemeSigners = new ArrayList<>(); + private final List<SignerInfo> mV2SchemeSigners = new ArrayList<>(); + private final List<SignerInfo> mV3SchemeSigners = new ArrayList<>(); + private final List<List<SignerInfo>> mAllSchemeSigners = Arrays.asList(mV1SchemeSigners, + mV2SchemeSigners, mV3SchemeSigners); + private SourceStampInfo mSourceStampInfo; + + private final List<ApkVerificationIssue> mErrors = new ArrayList<>(); + private final List<ApkVerificationIssue> mWarnings = new ArrayList<>(); + + private boolean mVerified; + + void addVerificationError(int errorId, Object... params) { + mErrors.add(new ApkVerificationIssue(errorId, params)); + } + + void addVerificationWarning(int warningId, Object... params) { + mWarnings.add(new ApkVerificationIssue(warningId, params)); + } + + private void addV1Signer(SignerInfo signerInfo) { + mV1SchemeSigners.add(signerInfo); + } + + private void addV2Signer(SignerInfo signerInfo) { + mV2SchemeSigners.add(signerInfo); + } + + private void addV3Signer(SignerInfo signerInfo) { + mV3SchemeSigners.add(signerInfo); + } + + /** + * Returns {@code true} if the APK's source stamp signature + */ + public boolean isVerified() { + return mVerified; + } + + private void mergeFrom(ApkSigResult source) { + switch (source.signatureSchemeVersion) { + case Constants.VERSION_SOURCE_STAMP: + mVerified = source.verified; + if (!source.mSigners.isEmpty()) { + mSourceStampInfo = new SourceStampInfo(source.mSigners.get(0)); + } + break; + default: + throw new IllegalArgumentException( + "Unknown ApkSigResult Signing Block Scheme Id " + + source.signatureSchemeVersion); + } + } + + /** + * Returns a {@code List} of {@link SignerInfo} objects representing the V1 signers of the + * provided APK. + */ + public List<SignerInfo> getV1SchemeSigners() { + return mV1SchemeSigners; + } + + /** + * Returns a {@code List} of {@link SignerInfo} objects representing the V2 signers of the + * provided APK. + */ + public List<SignerInfo> getV2SchemeSigners() { + return mV2SchemeSigners; + } + + /** + * Returns a {@code List} of {@link SignerInfo} objects representing the V3 signers of the + * provided APK. + */ + public List<SignerInfo> getV3SchemeSigners() { + return mV3SchemeSigners; + } + + /** + * Returns the {@link SourceStampInfo} instance representing the source stamp signer for the + * APK, or null if the source stamp signature verification failed before the stamp signature + * block could be fully parsed. + */ + public SourceStampInfo getSourceStampInfo() { + return mSourceStampInfo; + } + + /** + * Returns {@code true} if an error was encountered while verifying the APK. + * + * <p>Any error prevents the APK from being considered verified. + */ + public boolean containsErrors() { + if (!mErrors.isEmpty()) { + return true; + } + for (List<SignerInfo> signers : mAllSchemeSigners) { + for (SignerInfo signer : signers) { + if (signer.containsErrors()) { + return true; + } + } + } + if (mSourceStampInfo != null) { + if (mSourceStampInfo.containsErrors()) { + return true; + } + } + return false; + } + + /** + * Returns the errors encountered while verifying the APK's source stamp. + */ + public List<ApkVerificationIssue> getErrors() { + return mErrors; + } + + /** + * Returns the warnings encountered while verifying the APK's source stamp. + */ + public List<ApkVerificationIssue> getWarnings() { + return mWarnings; + } + + /** + * Returns all errors for this result, including any errors from signature scheme signers + * and the source stamp. + */ + public List<ApkVerificationIssue> getAllErrors() { + List<ApkVerificationIssue> errors = new ArrayList<>(); + errors.addAll(mErrors); + + for (List<SignerInfo> signers : mAllSchemeSigners) { + for (SignerInfo signer : signers) { + errors.addAll(signer.getErrors()); + } + } + if (mSourceStampInfo != null) { + errors.addAll(mSourceStampInfo.getErrors()); + } + return errors; + } + + /** + * Returns all warnings for this result, including any warnings from signature scheme + * signers and the source stamp. + */ + public List<ApkVerificationIssue> getAllWarnings() { + List<ApkVerificationIssue> warnings = new ArrayList<>(); + warnings.addAll(mWarnings); + + for (List<SignerInfo> signers : mAllSchemeSigners) { + for (SignerInfo signer : signers) { + warnings.addAll(signer.getWarnings()); + } + } + if (mSourceStampInfo != null) { + warnings.addAll(mSourceStampInfo.getWarnings()); + } + return warnings; + } + + /** + * Contains information about an APK's signer and any errors encountered while parsing the + * corresponding signature block. + */ + public static class SignerInfo { + private X509Certificate mSigningCertificate; + private final List<ApkVerificationIssue> mErrors = new ArrayList<>(); + private final List<ApkVerificationIssue> mWarnings = new ArrayList<>(); + + void setSigningCertificate(X509Certificate signingCertificate) { + mSigningCertificate = signingCertificate; + } + + void addVerificationError(int errorId, Object... params) { + mErrors.add(new ApkVerificationIssue(errorId, params)); + } + + void addVerificationWarning(int warningId, Object... params) { + mWarnings.add(new ApkVerificationIssue(warningId, params)); + } + + /** + * Returns the current signing certificate used by this signer. + */ + public X509Certificate getSigningCertificate() { + return mSigningCertificate; + } + + /** + * Returns a {@link List} of {@link ApkVerificationIssue} objects representing errors + * encountered during processing of this signer's signature block. + */ + public List<ApkVerificationIssue> getErrors() { + return mErrors; + } + + /** + * Returns a {@link List} of {@link ApkVerificationIssue} objects representing warnings + * encountered during processing of this signer's signature block. + */ + public List<ApkVerificationIssue> getWarnings() { + return mWarnings; + } + + /** + * Returns {@code true} if any errors were encountered while parsing this signer's + * signature block. + */ + public boolean containsErrors() { + return !mErrors.isEmpty(); + } + } + + /** + * Contains information about an APK's source stamp and any errors encountered while + * parsing the stamp signature block. + */ + public static class SourceStampInfo { + private final List<X509Certificate> mCertificates; + private final List<X509Certificate> mCertificateLineage; + + private final List<ApkVerificationIssue> mErrors = new ArrayList<>(); + private final List<ApkVerificationIssue> mWarnings = new ArrayList<>(); + private final List<ApkVerificationIssue> mInfoMessages = new ArrayList<>(); + + private final long mTimestamp; + + /* + * Since this utility is intended just to verify the source stamp, and the source stamp + * currently only logs warnings to prevent failing the APK signature verification, treat + * all warnings as errors. If the stamp verification is updated to log errors this + * should be set to false to ensure only errors trigger a failure verifying the source + * stamp. + */ + private static final boolean mWarningsAsErrors = true; + + private SourceStampInfo(ApkSignerInfo result) { + mCertificates = result.certs; + mCertificateLineage = result.certificateLineage; + mErrors.addAll(result.getErrors()); + mWarnings.addAll(result.getWarnings()); + mInfoMessages.addAll(result.getInfoMessages()); + mTimestamp = result.timestamp; + } + + /** + * Returns the SourceStamp's signing certificate or {@code null} if not available. The + * certificate is guaranteed to be available if no errors were encountered during + * verification (see {@link #containsErrors()}. + * + * <p>This certificate contains the SourceStamp's public key. + */ + public X509Certificate getCertificate() { + return mCertificates.isEmpty() ? null : mCertificates.get(0); + } + + /** + * Returns a {@code List} of {@link X509Certificate} instances representing the source + * stamp signer's lineage with the oldest signer at element 0, or an empty {@code List} + * if the stamp's signing certificate has not been rotated. + */ + public List<X509Certificate> getCertificatesInLineage() { + return mCertificateLineage; + } + + /** + * Returns whether any errors were encountered during the source stamp verification. + */ + public boolean containsErrors() { + return !mErrors.isEmpty() || (mWarningsAsErrors && !mWarnings.isEmpty()); + } + + /** + * Returns {@code true} if any info messages were encountered during verification of + * this source stamp. + */ + public boolean containsInfoMessages() { + return !mInfoMessages.isEmpty(); + } + + /** + * Returns a {@code List} of {@link ApkVerificationIssue} representing errors that were + * encountered during source stamp verification. + */ + public List<ApkVerificationIssue> getErrors() { + if (!mWarningsAsErrors) { + return mErrors; + } + List<ApkVerificationIssue> result = new ArrayList<>(); + result.addAll(mErrors); + result.addAll(mWarnings); + return result; + } + + /** + * Returns a {@code List} of {@link ApkVerificationIssue} representing warnings that + * were encountered during source stamp verification. + */ + public List<ApkVerificationIssue> getWarnings() { + return mWarnings; + } + + /** + * Returns a {@code List} of {@link ApkVerificationIssue} representing info messages + * that were encountered during source stamp verification. + */ + public List<ApkVerificationIssue> getInfoMessages() { + return mInfoMessages; + } + + /** + * Returns the epoch timestamp in seconds representing the time this source stamp block + * was signed, or 0 if the timestamp is not available. + */ + public long getTimestampEpochSeconds() { + return mTimestamp; + } + } + } + + /** + * Builder of {@link SourceStampVerifier} instances. + * + * <p> The resulting verifier, by default, checks whether the APK's source stamp signature will + * verify on all platform versions. The APK's {@code android:minSdkVersion} attribute is not + * queried to determine the APK's minimum supported level, so the caller should specify a lower + * bound with {@link #setMinCheckedPlatformVersion(int)}. + */ + public static class Builder { + private final File mApkFile; + private final DataSource mApkDataSource; + + private int mMinSdkVersion = 1; + private int mMaxSdkVersion = Integer.MAX_VALUE; + + /** + * Constructs a new {@code Builder} for source stamp verification of the provided {@code + * apk}. + */ + public Builder(File apk) { + if (apk == null) { + throw new NullPointerException("apk == null"); + } + mApkFile = apk; + mApkDataSource = null; + } + + /** + * Constructs a new {@code Builder} for source stamp verification of the provided {@code + * apk}. + */ + public Builder(DataSource apk) { + if (apk == null) { + throw new NullPointerException("apk == null"); + } + mApkDataSource = apk; + mApkFile = null; + } + + /** + * Sets the oldest Android platform version for which the APK's source stamp is verified. + * + * <p>APK source stamp verification will confirm that the APK's stamp is expected to verify + * on all Android platforms starting from the platform version with the provided {@code + * minSdkVersion}. The upper end of the platform versions range can be modified via + * {@link #setMaxCheckedPlatformVersion(int)}. + * + * @param minSdkVersion API Level of the oldest platform for which to verify the APK + */ + public SourceStampVerifier.Builder setMinCheckedPlatformVersion(int minSdkVersion) { + mMinSdkVersion = minSdkVersion; + return this; + } + + /** + * Sets the newest Android platform version for which the APK's source stamp is verified. + * + * <p>APK source stamp verification will confirm that the APK's stamp is expected to verify + * on all platform versions up to and including the proviced {@code maxSdkVersion}. The + * lower end of the platform versions range can be modified via {@link + * #setMinCheckedPlatformVersion(int)}. + * + * @param maxSdkVersion API Level of the newest platform for which to verify the APK + * @see #setMinCheckedPlatformVersion(int) + */ + public SourceStampVerifier.Builder setMaxCheckedPlatformVersion(int maxSdkVersion) { + mMaxSdkVersion = maxSdkVersion; + return this; + } + + /** + * Returns a {@link SourceStampVerifier} initialized according to the configuration of this + * builder. + */ + public SourceStampVerifier build() { + return new SourceStampVerifier( + mApkFile, + mApkDataSource, + mMinSdkVersion, + mMaxSdkVersion); + } + } +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/apk/ApkFormatException.java b/platform/android/java/editor/src/main/java/com/android/apksig/apk/ApkFormatException.java new file mode 100644 index 0000000000..a780134498 --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/apk/ApkFormatException.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.apk; + +/** + * Indicates that an APK is not well-formed. For example, this may indicate that the APK is not a + * well-formed ZIP archive, in which case {@link #getCause()} will return a + * {@link com.android.apksig.zip.ZipFormatException ZipFormatException}, or that the APK contains + * multiple ZIP entries with the same name. + */ +public class ApkFormatException extends Exception { + private static final long serialVersionUID = 1L; + + public ApkFormatException(String message) { + super(message); + } + + public ApkFormatException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/apk/ApkSigningBlockNotFoundException.java b/platform/android/java/editor/src/main/java/com/android/apksig/apk/ApkSigningBlockNotFoundException.java new file mode 100644 index 0000000000..fd961d5716 --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/apk/ApkSigningBlockNotFoundException.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.apk; + +/** + * Indicates that no APK Signing Block was found in an APK. + */ +public class ApkSigningBlockNotFoundException extends Exception { + private static final long serialVersionUID = 1L; + + public ApkSigningBlockNotFoundException(String message) { + super(message); + } + + public ApkSigningBlockNotFoundException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/apk/ApkUtils.java b/platform/android/java/editor/src/main/java/com/android/apksig/apk/ApkUtils.java new file mode 100644 index 0000000000..156ea17c00 --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/apk/ApkUtils.java @@ -0,0 +1,670 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.apk; + +import com.android.apksig.internal.apk.AndroidBinXmlParser; +import com.android.apksig.internal.apk.stamp.SourceStampConstants; +import com.android.apksig.internal.apk.v1.V1SchemeVerifier; +import com.android.apksig.internal.util.Pair; +import com.android.apksig.internal.zip.CentralDirectoryRecord; +import com.android.apksig.internal.zip.LocalFileRecord; +import com.android.apksig.internal.zip.ZipUtils; +import com.android.apksig.util.DataSource; +import com.android.apksig.zip.ZipFormatException; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; + +/** + * APK utilities. + */ +public abstract class ApkUtils { + + /** + * Name of the Android manifest ZIP entry in APKs. + */ + public static final String ANDROID_MANIFEST_ZIP_ENTRY_NAME = "AndroidManifest.xml"; + + /** Name of the SourceStamp certificate hash ZIP entry in APKs. */ + public static final String SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME = + SourceStampConstants.SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME; + + private ApkUtils() {} + + /** + * Finds the main ZIP sections of the provided APK. + * + * @throws IOException if an I/O error occurred while reading the APK + * @throws ZipFormatException if the APK is malformed + */ + public static ZipSections findZipSections(DataSource apk) + throws IOException, ZipFormatException { + com.android.apksig.zip.ZipSections zipSections = ApkUtilsLite.findZipSections(apk); + return new ZipSections( + zipSections.getZipCentralDirectoryOffset(), + zipSections.getZipCentralDirectorySizeBytes(), + zipSections.getZipCentralDirectoryRecordCount(), + zipSections.getZipEndOfCentralDirectoryOffset(), + zipSections.getZipEndOfCentralDirectory()); + } + + /** + * Information about the ZIP sections of an APK. + */ + public static class ZipSections extends com.android.apksig.zip.ZipSections { + public ZipSections( + long centralDirectoryOffset, + long centralDirectorySizeBytes, + int centralDirectoryRecordCount, + long eocdOffset, + ByteBuffer eocd) { + super(centralDirectoryOffset, centralDirectorySizeBytes, centralDirectoryRecordCount, + eocdOffset, eocd); + } + } + + /** + * Sets the offset of the start of the ZIP Central Directory in the APK's ZIP End of Central + * Directory record. + * + * @param zipEndOfCentralDirectory APK's ZIP End of Central Directory record + * @param offset offset of the ZIP Central Directory relative to the start of the archive. Must + * be between {@code 0} and {@code 2^32 - 1} inclusive. + */ + public static void setZipEocdCentralDirectoryOffset( + ByteBuffer zipEndOfCentralDirectory, long offset) { + ByteBuffer eocd = zipEndOfCentralDirectory.slice(); + eocd.order(ByteOrder.LITTLE_ENDIAN); + ZipUtils.setZipEocdCentralDirectoryOffset(eocd, offset); + } + + /** + * Updates the length of EOCD comment. + * + * @param zipEndOfCentralDirectory APK's ZIP End of Central Directory record + */ + public static void updateZipEocdCommentLen(ByteBuffer zipEndOfCentralDirectory) { + ByteBuffer eocd = zipEndOfCentralDirectory.slice(); + eocd.order(ByteOrder.LITTLE_ENDIAN); + ZipUtils.updateZipEocdCommentLen(eocd); + } + + /** + * Returns the APK Signing Block of the provided {@code apk}. + * + * @throws ApkFormatException if the APK is not a valid ZIP archive + * @throws IOException if an I/O error occurs + * @throws ApkSigningBlockNotFoundException if there is no APK Signing Block in the APK + * + * @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2 + * </a> + */ + public static ApkSigningBlock findApkSigningBlock(DataSource apk) + throws ApkFormatException, IOException, ApkSigningBlockNotFoundException { + ApkUtils.ZipSections inputZipSections; + try { + inputZipSections = ApkUtils.findZipSections(apk); + } catch (ZipFormatException e) { + throw new ApkFormatException("Malformed APK: not a ZIP archive", e); + } + return findApkSigningBlock(apk, inputZipSections); + } + + /** + * Returns the APK Signing Block of the provided APK. + * + * @throws IOException if an I/O error occurs + * @throws ApkSigningBlockNotFoundException if there is no APK Signing Block in the APK + * + * @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2 + * </a> + */ + public static ApkSigningBlock findApkSigningBlock(DataSource apk, ZipSections zipSections) + throws IOException, ApkSigningBlockNotFoundException { + ApkUtilsLite.ApkSigningBlock apkSigningBlock = ApkUtilsLite.findApkSigningBlock(apk, + zipSections); + return new ApkSigningBlock(apkSigningBlock.getStartOffset(), apkSigningBlock.getContents()); + } + + /** + * Information about the location of the APK Signing Block inside an APK. + */ + public static class ApkSigningBlock extends ApkUtilsLite.ApkSigningBlock { + /** + * Constructs a new {@code ApkSigningBlock}. + * + * @param startOffsetInApk start offset (in bytes, relative to start of file) of the APK + * Signing Block inside the APK file + * @param contents contents of the APK Signing Block + */ + public ApkSigningBlock(long startOffsetInApk, DataSource contents) { + super(startOffsetInApk, contents); + } + } + + /** + * Returns the contents of the APK's {@code AndroidManifest.xml}. + * + * @throws IOException if an I/O error occurs while reading the APK + * @throws ApkFormatException if the APK is malformed + */ + public static ByteBuffer getAndroidManifest(DataSource apk) + throws IOException, ApkFormatException { + ZipSections zipSections; + try { + zipSections = findZipSections(apk); + } catch (ZipFormatException e) { + throw new ApkFormatException("Not a valid ZIP archive", e); + } + List<CentralDirectoryRecord> cdRecords = + V1SchemeVerifier.parseZipCentralDirectory(apk, zipSections); + CentralDirectoryRecord androidManifestCdRecord = null; + for (CentralDirectoryRecord cdRecord : cdRecords) { + if (ANDROID_MANIFEST_ZIP_ENTRY_NAME.equals(cdRecord.getName())) { + androidManifestCdRecord = cdRecord; + break; + } + } + if (androidManifestCdRecord == null) { + throw new ApkFormatException("Missing " + ANDROID_MANIFEST_ZIP_ENTRY_NAME); + } + DataSource lfhSection = apk.slice(0, zipSections.getZipCentralDirectoryOffset()); + + try { + return ByteBuffer.wrap( + LocalFileRecord.getUncompressedData( + lfhSection, androidManifestCdRecord, lfhSection.size())); + } catch (ZipFormatException e) { + throw new ApkFormatException("Failed to read " + ANDROID_MANIFEST_ZIP_ENTRY_NAME, e); + } + } + + /** + * Android resource ID of the {@code android:minSdkVersion} attribute in AndroidManifest.xml. + */ + private static final int MIN_SDK_VERSION_ATTR_ID = 0x0101020c; + + /** + * Android resource ID of the {@code android:debuggable} attribute in AndroidManifest.xml. + */ + private static final int DEBUGGABLE_ATTR_ID = 0x0101000f; + + /** + * Android resource ID of the {@code android:targetSandboxVersion} attribute in + * AndroidManifest.xml. + */ + private static final int TARGET_SANDBOX_VERSION_ATTR_ID = 0x0101054c; + + /** + * Android resource ID of the {@code android:targetSdkVersion} attribute in + * AndroidManifest.xml. + */ + private static final int TARGET_SDK_VERSION_ATTR_ID = 0x01010270; + private static final String USES_SDK_ELEMENT_TAG = "uses-sdk"; + + /** + * Android resource ID of the {@code android:versionCode} attribute in AndroidManifest.xml. + */ + private static final int VERSION_CODE_ATTR_ID = 0x0101021b; + private static final String MANIFEST_ELEMENT_TAG = "manifest"; + + /** + * Android resource ID of the {@code android:versionCodeMajor} attribute in AndroidManifest.xml. + */ + private static final int VERSION_CODE_MAJOR_ATTR_ID = 0x01010576; + + /** + * Returns the lowest Android platform version (API Level) supported by an APK with the + * provided {@code AndroidManifest.xml}. + * + * @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android + * resource format + * + * @throws MinSdkVersionException if an error occurred while determining the API Level + */ + public static int getMinSdkVersionFromBinaryAndroidManifest( + ByteBuffer androidManifestContents) throws MinSdkVersionException { + // IMPLEMENTATION NOTE: Minimum supported Android platform version number is declared using + // uses-sdk elements which are children of the top-level manifest element. uses-sdk element + // declares the minimum supported platform version using the android:minSdkVersion attribute + // whose default value is 1. + // For each encountered uses-sdk element, the Android runtime checks that its minSdkVersion + // is not higher than the runtime's API Level and rejects APKs if it is higher. Thus, the + // effective minSdkVersion value is the maximum over the encountered minSdkVersion values. + + try { + // If no uses-sdk elements are encountered, Android accepts the APK. We treat this + // scenario as though the minimum supported API Level is 1. + int result = 1; + + AndroidBinXmlParser parser = new AndroidBinXmlParser(androidManifestContents); + int eventType = parser.getEventType(); + while (eventType != AndroidBinXmlParser.EVENT_END_DOCUMENT) { + if ((eventType == AndroidBinXmlParser.EVENT_START_ELEMENT) + && (parser.getDepth() == 2) + && ("uses-sdk".equals(parser.getName())) + && (parser.getNamespace().isEmpty())) { + // In each uses-sdk element, minSdkVersion defaults to 1 + int minSdkVersion = 1; + for (int i = 0; i < parser.getAttributeCount(); i++) { + if (parser.getAttributeNameResourceId(i) == MIN_SDK_VERSION_ATTR_ID) { + int valueType = parser.getAttributeValueType(i); + switch (valueType) { + case AndroidBinXmlParser.VALUE_TYPE_INT: + minSdkVersion = parser.getAttributeIntValue(i); + break; + case AndroidBinXmlParser.VALUE_TYPE_STRING: + minSdkVersion = + getMinSdkVersionForCodename( + parser.getAttributeStringValue(i)); + break; + default: + throw new MinSdkVersionException( + "Unable to determine APK's minimum supported Android" + + ": unsupported value type in " + + ANDROID_MANIFEST_ZIP_ENTRY_NAME + "'s" + + " minSdkVersion" + + ". Only integer values supported."); + } + break; + } + } + result = Math.max(result, minSdkVersion); + } + eventType = parser.next(); + } + + return result; + } catch (AndroidBinXmlParser.XmlParserException e) { + throw new MinSdkVersionException( + "Unable to determine APK's minimum supported Android platform version" + + ": malformed binary resource: " + ANDROID_MANIFEST_ZIP_ENTRY_NAME, + e); + } + } + + private static class CodenamesLazyInitializer { + + /** + * List of platform codename (first letter of) to API Level mappings. The list must be + * sorted by the first letter. For codenames not in the list, the assumption is that the API + * Level is incremented by one for every increase in the codename's first letter. + */ + @SuppressWarnings({"rawtypes", "unchecked"}) + private static final Pair<Character, Integer>[] SORTED_CODENAMES_FIRST_CHAR_TO_API_LEVEL = + new Pair[] { + Pair.of('C', 2), + Pair.of('D', 3), + Pair.of('E', 4), + Pair.of('F', 7), + Pair.of('G', 8), + Pair.of('H', 10), + Pair.of('I', 13), + Pair.of('J', 15), + Pair.of('K', 18), + Pair.of('L', 20), + Pair.of('M', 22), + Pair.of('N', 23), + Pair.of('O', 25), + }; + + private static final Comparator<Pair<Character, Integer>> CODENAME_FIRST_CHAR_COMPARATOR = + new ByFirstComparator(); + + private static class ByFirstComparator implements Comparator<Pair<Character, Integer>> { + @Override + public int compare(Pair<Character, Integer> o1, Pair<Character, Integer> o2) { + char c1 = o1.getFirst(); + char c2 = o2.getFirst(); + return c1 - c2; + } + } + } + + /** + * Returns the API Level corresponding to the provided platform codename. + * + * <p>This method is pessimistic. It returns a value one lower than the API Level with which the + * platform is actually released (e.g., 23 for N which was released as API Level 24). This is + * because new features which first appear in an API Level are not available in the early days + * of that platform version's existence, when the platform only has a codename. Moreover, this + * method currently doesn't differentiate between initial and MR releases, meaning API Level + * returned for MR releases may be more than one lower than the API Level with which the + * platform version is actually released. + * + * @throws CodenameMinSdkVersionException if the {@code codename} is not supported + */ + static int getMinSdkVersionForCodename(String codename) throws CodenameMinSdkVersionException { + char firstChar = codename.isEmpty() ? ' ' : codename.charAt(0); + // Codenames are case-sensitive. Only codenames starting with A-Z are supported for now. + // We only look at the first letter of the codename as this is the most important letter. + if ((firstChar >= 'A') && (firstChar <= 'Z')) { + Pair<Character, Integer>[] sortedCodenamesFirstCharToApiLevel = + CodenamesLazyInitializer.SORTED_CODENAMES_FIRST_CHAR_TO_API_LEVEL; + int searchResult = + Arrays.binarySearch( + sortedCodenamesFirstCharToApiLevel, + Pair.of(firstChar, null), // second element of the pair is ignored here + CodenamesLazyInitializer.CODENAME_FIRST_CHAR_COMPARATOR); + if (searchResult >= 0) { + // Exact match -- searchResult is the index of the matching element + return sortedCodenamesFirstCharToApiLevel[searchResult].getSecond(); + } + // Not an exact match -- searchResult is negative and is -(insertion index) - 1. + // The element at insertionIndex - 1 (if present) is smaller than firstChar and the + // element at insertionIndex (if present) is greater than firstChar. + int insertionIndex = -1 - searchResult; // insertionIndex is in [0; array length] + if (insertionIndex == 0) { + // 'A' or 'B' -- never released to public + return 1; + } else { + // The element at insertionIndex - 1 is the newest older codename. + // API Level bumped by at least 1 for every change in the first letter of codename + Pair<Character, Integer> newestOlderCodenameMapping = + sortedCodenamesFirstCharToApiLevel[insertionIndex - 1]; + char newestOlderCodenameFirstChar = newestOlderCodenameMapping.getFirst(); + int newestOlderCodenameApiLevel = newestOlderCodenameMapping.getSecond(); + return newestOlderCodenameApiLevel + (firstChar - newestOlderCodenameFirstChar); + } + } + + throw new CodenameMinSdkVersionException( + "Unable to determine APK's minimum supported Android platform version" + + " : Unsupported codename in " + ANDROID_MANIFEST_ZIP_ENTRY_NAME + + "'s minSdkVersion: \"" + codename + "\"", + codename); + } + + /** + * Returns {@code true} if the APK is debuggable according to its {@code AndroidManifest.xml}. + * See the {@code android:debuggable} attribute of the {@code application} element. + * + * @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android + * resource format + * + * @throws ApkFormatException if the manifest is malformed + */ + public static boolean getDebuggableFromBinaryAndroidManifest( + ByteBuffer androidManifestContents) throws ApkFormatException { + // IMPLEMENTATION NOTE: Whether the package is debuggable is declared using the first + // "application" element which is a child of the top-level manifest element. The debuggable + // attribute of this application element is coerced to a boolean value. If there is no + // application element or if it doesn't declare the debuggable attribute, the package is + // considered not debuggable. + + try { + AndroidBinXmlParser parser = new AndroidBinXmlParser(androidManifestContents); + int eventType = parser.getEventType(); + while (eventType != AndroidBinXmlParser.EVENT_END_DOCUMENT) { + if ((eventType == AndroidBinXmlParser.EVENT_START_ELEMENT) + && (parser.getDepth() == 2) + && ("application".equals(parser.getName())) + && (parser.getNamespace().isEmpty())) { + for (int i = 0; i < parser.getAttributeCount(); i++) { + if (parser.getAttributeNameResourceId(i) == DEBUGGABLE_ATTR_ID) { + int valueType = parser.getAttributeValueType(i); + switch (valueType) { + case AndroidBinXmlParser.VALUE_TYPE_BOOLEAN: + case AndroidBinXmlParser.VALUE_TYPE_STRING: + case AndroidBinXmlParser.VALUE_TYPE_INT: + String value = parser.getAttributeStringValue(i); + return ("true".equals(value)) + || ("TRUE".equals(value)) + || ("1".equals(value)); + case AndroidBinXmlParser.VALUE_TYPE_REFERENCE: + // References to resources are not supported on purpose. The + // reason is that the resolved value depends on the resource + // configuration (e.g, MNC/MCC, locale, screen density) used + // at resolution time. As a result, the same APK may appear as + // debuggable in one situation and as non-debuggable in another + // situation. Such APKs may put users at risk. + throw new ApkFormatException( + "Unable to determine whether APK is debuggable" + + ": " + ANDROID_MANIFEST_ZIP_ENTRY_NAME + "'s" + + " android:debuggable attribute references a" + + " resource. References are not supported for" + + " security reasons. Only constant boolean," + + " string and int values are supported."); + default: + throw new ApkFormatException( + "Unable to determine whether APK is debuggable" + + ": " + ANDROID_MANIFEST_ZIP_ENTRY_NAME + "'s" + + " android:debuggable attribute uses" + + " unsupported value type. Only boolean," + + " string and int values are supported."); + } + } + } + // This application element does not declare the debuggable attribute + return false; + } + eventType = parser.next(); + } + + // No application element found + return false; + } catch (AndroidBinXmlParser.XmlParserException e) { + throw new ApkFormatException( + "Unable to determine whether APK is debuggable: malformed binary resource: " + + ANDROID_MANIFEST_ZIP_ENTRY_NAME, + e); + } + } + + /** + * Returns the package name of the APK according to its {@code AndroidManifest.xml} or + * {@code null} if package name is not declared. See the {@code package} attribute of the + * {@code manifest} element. + * + * @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android + * resource format + * + * @throws ApkFormatException if the manifest is malformed + */ + public static String getPackageNameFromBinaryAndroidManifest( + ByteBuffer androidManifestContents) throws ApkFormatException { + // IMPLEMENTATION NOTE: Package name is declared as the "package" attribute of the top-level + // manifest element. Interestingly, as opposed to most other attributes, Android Package + // Manager looks up this attribute by its name rather than by its resource ID. + + try { + AndroidBinXmlParser parser = new AndroidBinXmlParser(androidManifestContents); + int eventType = parser.getEventType(); + while (eventType != AndroidBinXmlParser.EVENT_END_DOCUMENT) { + if ((eventType == AndroidBinXmlParser.EVENT_START_ELEMENT) + && (parser.getDepth() == 1) + && ("manifest".equals(parser.getName())) + && (parser.getNamespace().isEmpty())) { + for (int i = 0; i < parser.getAttributeCount(); i++) { + if ("package".equals(parser.getAttributeName(i)) + && (parser.getNamespace().isEmpty())) { + return parser.getAttributeStringValue(i); + } + } + // No "package" attribute found + return null; + } + eventType = parser.next(); + } + + // No manifest element found + return null; + } catch (AndroidBinXmlParser.XmlParserException e) { + throw new ApkFormatException( + "Unable to determine APK package name: malformed binary resource: " + + ANDROID_MANIFEST_ZIP_ENTRY_NAME, + e); + } + } + + /** + * Returns the security sandbox version targeted by an APK with the provided + * {@code AndroidManifest.xml}. + * + * <p>If the security sandbox version is not specified in the manifest a default value of 1 is + * returned. + * + * @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android + * resource format + */ + public static int getTargetSandboxVersionFromBinaryAndroidManifest( + ByteBuffer androidManifestContents) { + try { + return getAttributeValueFromBinaryAndroidManifest(androidManifestContents, + MANIFEST_ELEMENT_TAG, TARGET_SANDBOX_VERSION_ATTR_ID); + } catch (ApkFormatException e) { + // An ApkFormatException indicates the target sandbox is not specified in the manifest; + // return a default value of 1. + return 1; + } + } + + /** + * Returns the SDK version targeted by an APK with the provided {@code AndroidManifest.xml}. + * + * <p>If the targetSdkVersion is not specified the minimumSdkVersion is returned. If neither + * value is specified then a value of 1 is returned. + * + * @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android + * resource format + */ + public static int getTargetSdkVersionFromBinaryAndroidManifest( + ByteBuffer androidManifestContents) { + // If the targetSdkVersion is not specified then the platform will use the value of the + // minSdkVersion; if neither is specified then the platform will use a value of 1. + int minSdkVersion = 1; + try { + return getAttributeValueFromBinaryAndroidManifest(androidManifestContents, + USES_SDK_ELEMENT_TAG, TARGET_SDK_VERSION_ATTR_ID); + } catch (ApkFormatException e) { + // Expected if the APK does not contain a targetSdkVersion attribute or the uses-sdk + // element is not specified at all. + } + androidManifestContents.rewind(); + try { + minSdkVersion = getMinSdkVersionFromBinaryAndroidManifest(androidManifestContents); + } catch (ApkFormatException e) { + // Similar to above, expected if the APK does not contain a minSdkVersion attribute, or + // the uses-sdk element is not specified at all. + } + return minSdkVersion; + } + + /** + * Returns the versionCode of the APK according to its {@code AndroidManifest.xml}. + * + * <p>If the versionCode is not specified in the {@code AndroidManifest.xml} or is not a valid + * integer an ApkFormatException is thrown. + * + * @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android + * resource format + * @throws ApkFormatException if an error occurred while determining the versionCode, or if the + * versionCode attribute value is not available. + */ + public static int getVersionCodeFromBinaryAndroidManifest(ByteBuffer androidManifestContents) + throws ApkFormatException { + return getAttributeValueFromBinaryAndroidManifest(androidManifestContents, + MANIFEST_ELEMENT_TAG, VERSION_CODE_ATTR_ID); + } + + /** + * Returns the versionCode and versionCodeMajor of the APK according to its {@code + * AndroidManifest.xml} combined together as a single long value. + * + * <p>The versionCodeMajor is placed in the upper 32 bits, and the versionCode is in the lower + * 32 bits. If the versionCodeMajor is not specified then the versionCode is returned. + * + * @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android + * resource format + * @throws ApkFormatException if an error occurred while determining the version, or if the + * versionCode attribute value is not available. + */ + public static long getLongVersionCodeFromBinaryAndroidManifest( + ByteBuffer androidManifestContents) throws ApkFormatException { + // If the versionCode is not found then allow the ApkFormatException to be thrown to notify + // the caller that the versionCode is not available. + int versionCode = getVersionCodeFromBinaryAndroidManifest(androidManifestContents); + long versionCodeMajor = 0; + try { + androidManifestContents.rewind(); + versionCodeMajor = getAttributeValueFromBinaryAndroidManifest(androidManifestContents, + MANIFEST_ELEMENT_TAG, VERSION_CODE_MAJOR_ATTR_ID); + } catch (ApkFormatException e) { + // This is expected if the versionCodeMajor has not been defined for the APK; in this + // case the return value is just the versionCode. + } + return (versionCodeMajor << 32) | versionCode; + } + + /** + * Returns the integer value of the requested {@code attributeId} in the specified {@code + * elementName} from the provided {@code androidManifestContents} in binary Android resource + * format. + * + * @throws ApkFormatException if an error occurred while attempting to obtain the attribute, or + * if the requested attribute is not found. + */ + private static int getAttributeValueFromBinaryAndroidManifest( + ByteBuffer androidManifestContents, String elementName, int attributeId) + throws ApkFormatException { + if (elementName == null) { + throw new NullPointerException("elementName cannot be null"); + } + try { + AndroidBinXmlParser parser = new AndroidBinXmlParser(androidManifestContents); + int eventType = parser.getEventType(); + while (eventType != AndroidBinXmlParser.EVENT_END_DOCUMENT) { + if ((eventType == AndroidBinXmlParser.EVENT_START_ELEMENT) + && (elementName.equals(parser.getName()))) { + for (int i = 0; i < parser.getAttributeCount(); i++) { + if (parser.getAttributeNameResourceId(i) == attributeId) { + int valueType = parser.getAttributeValueType(i); + switch (valueType) { + case AndroidBinXmlParser.VALUE_TYPE_INT: + case AndroidBinXmlParser.VALUE_TYPE_STRING: + return parser.getAttributeIntValue(i); + default: + throw new ApkFormatException( + "Unsupported value type, " + valueType + + ", for attribute " + String.format("0x%08X", + attributeId) + " under element " + elementName); + + } + } + } + } + eventType = parser.next(); + } + throw new ApkFormatException( + "Failed to determine APK's " + elementName + " attribute " + + String.format("0x%08X", attributeId) + " value"); + } catch (AndroidBinXmlParser.XmlParserException e) { + throw new ApkFormatException( + "Unable to determine value for attribute " + String.format("0x%08X", + attributeId) + " under element " + elementName + + "; malformed binary resource: " + ANDROID_MANIFEST_ZIP_ENTRY_NAME, e); + } + } + + public static byte[] computeSha256DigestBytes(byte[] data) { + return ApkUtilsLite.computeSha256DigestBytes(data); + } +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/apk/ApkUtilsLite.java b/platform/android/java/editor/src/main/java/com/android/apksig/apk/ApkUtilsLite.java new file mode 100644 index 0000000000..13f230119d --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/apk/ApkUtilsLite.java @@ -0,0 +1,199 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.apk; + +import com.android.apksig.internal.util.Pair; +import com.android.apksig.internal.zip.ZipUtils; +import com.android.apksig.util.DataSource; +import com.android.apksig.zip.ZipFormatException; +import com.android.apksig.zip.ZipSections; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +/** + * Lightweight version of the ApkUtils for clients that only require a subset of the utility + * functionality. + */ +public class ApkUtilsLite { + private ApkUtilsLite() {} + + /** + * Finds the main ZIP sections of the provided APK. + * + * @throws IOException if an I/O error occurred while reading the APK + * @throws ZipFormatException if the APK is malformed + */ + public static ZipSections findZipSections(DataSource apk) + throws IOException, ZipFormatException { + Pair<ByteBuffer, Long> eocdAndOffsetInFile = + ZipUtils.findZipEndOfCentralDirectoryRecord(apk); + if (eocdAndOffsetInFile == null) { + throw new ZipFormatException("ZIP End of Central Directory record not found"); + } + + ByteBuffer eocdBuf = eocdAndOffsetInFile.getFirst(); + long eocdOffset = eocdAndOffsetInFile.getSecond(); + eocdBuf.order(ByteOrder.LITTLE_ENDIAN); + long cdStartOffset = ZipUtils.getZipEocdCentralDirectoryOffset(eocdBuf); + if (cdStartOffset > eocdOffset) { + throw new ZipFormatException( + "ZIP Central Directory start offset out of range: " + cdStartOffset + + ". ZIP End of Central Directory offset: " + eocdOffset); + } + + long cdSizeBytes = ZipUtils.getZipEocdCentralDirectorySizeBytes(eocdBuf); + long cdEndOffset = cdStartOffset + cdSizeBytes; + if (cdEndOffset > eocdOffset) { + throw new ZipFormatException( + "ZIP Central Directory overlaps with End of Central Directory" + + ". CD end: " + cdEndOffset + + ", EoCD start: " + eocdOffset); + } + + int cdRecordCount = ZipUtils.getZipEocdCentralDirectoryTotalRecordCount(eocdBuf); + + return new ZipSections( + cdStartOffset, + cdSizeBytes, + cdRecordCount, + eocdOffset, + eocdBuf); + } + + // See https://source.android.com/security/apksigning/v2.html + private static final long APK_SIG_BLOCK_MAGIC_HI = 0x3234206b636f6c42L; + private static final long APK_SIG_BLOCK_MAGIC_LO = 0x20676953204b5041L; + private static final int APK_SIG_BLOCK_MIN_SIZE = 32; + + /** + * Returns the APK Signing Block of the provided APK. + * + * @throws IOException if an I/O error occurs + * @throws ApkSigningBlockNotFoundException if there is no APK Signing Block in the APK + * + * @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2 + * </a> + */ + public static ApkSigningBlock findApkSigningBlock(DataSource apk, ZipSections zipSections) + throws IOException, ApkSigningBlockNotFoundException { + // FORMAT (see https://source.android.com/security/apksigning/v2.html): + // OFFSET DATA TYPE DESCRIPTION + // * @+0 bytes uint64: size in bytes (excluding this field) + // * @+8 bytes payload + // * @-24 bytes uint64: size in bytes (same as the one above) + // * @-16 bytes uint128: magic + + long centralDirStartOffset = zipSections.getZipCentralDirectoryOffset(); + long centralDirEndOffset = + centralDirStartOffset + zipSections.getZipCentralDirectorySizeBytes(); + long eocdStartOffset = zipSections.getZipEndOfCentralDirectoryOffset(); + if (centralDirEndOffset != eocdStartOffset) { + throw new ApkSigningBlockNotFoundException( + "ZIP Central Directory is not immediately followed by End of Central Directory" + + ". CD end: " + centralDirEndOffset + + ", EoCD start: " + eocdStartOffset); + } + + if (centralDirStartOffset < APK_SIG_BLOCK_MIN_SIZE) { + throw new ApkSigningBlockNotFoundException( + "APK too small for APK Signing Block. ZIP Central Directory offset: " + + centralDirStartOffset); + } + // Read the magic and offset in file from the footer section of the block: + // * uint64: size of block + // * 16 bytes: magic + ByteBuffer footer = apk.getByteBuffer(centralDirStartOffset - 24, 24); + footer.order(ByteOrder.LITTLE_ENDIAN); + if ((footer.getLong(8) != APK_SIG_BLOCK_MAGIC_LO) + || (footer.getLong(16) != APK_SIG_BLOCK_MAGIC_HI)) { + throw new ApkSigningBlockNotFoundException( + "No APK Signing Block before ZIP Central Directory"); + } + // Read and compare size fields + long apkSigBlockSizeInFooter = footer.getLong(0); + if ((apkSigBlockSizeInFooter < footer.capacity()) + || (apkSigBlockSizeInFooter > Integer.MAX_VALUE - 8)) { + throw new ApkSigningBlockNotFoundException( + "APK Signing Block size out of range: " + apkSigBlockSizeInFooter); + } + int totalSize = (int) (apkSigBlockSizeInFooter + 8); + long apkSigBlockOffset = centralDirStartOffset - totalSize; + if (apkSigBlockOffset < 0) { + throw new ApkSigningBlockNotFoundException( + "APK Signing Block offset out of range: " + apkSigBlockOffset); + } + ByteBuffer apkSigBlock = apk.getByteBuffer(apkSigBlockOffset, 8); + apkSigBlock.order(ByteOrder.LITTLE_ENDIAN); + long apkSigBlockSizeInHeader = apkSigBlock.getLong(0); + if (apkSigBlockSizeInHeader != apkSigBlockSizeInFooter) { + throw new ApkSigningBlockNotFoundException( + "APK Signing Block sizes in header and footer do not match: " + + apkSigBlockSizeInHeader + " vs " + apkSigBlockSizeInFooter); + } + return new ApkSigningBlock(apkSigBlockOffset, apk.slice(apkSigBlockOffset, totalSize)); + } + + /** + * Information about the location of the APK Signing Block inside an APK. + */ + public static class ApkSigningBlock { + private final long mStartOffsetInApk; + private final DataSource mContents; + + /** + * Constructs a new {@code ApkSigningBlock}. + * + * @param startOffsetInApk start offset (in bytes, relative to start of file) of the APK + * Signing Block inside the APK file + * @param contents contents of the APK Signing Block + */ + public ApkSigningBlock(long startOffsetInApk, DataSource contents) { + mStartOffsetInApk = startOffsetInApk; + mContents = contents; + } + + /** + * Returns the start offset (in bytes, relative to start of file) of the APK Signing Block. + */ + public long getStartOffset() { + return mStartOffsetInApk; + } + + /** + * Returns the data source which provides the full contents of the APK Signing Block, + * including its footer. + */ + public DataSource getContents() { + return mContents; + } + } + + public static byte[] computeSha256DigestBytes(byte[] data) { + MessageDigest messageDigest; + try { + messageDigest = MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("SHA-256 is not found", e); + } + messageDigest.update(data); + return messageDigest.digest(); + } +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/apk/CodenameMinSdkVersionException.java b/platform/android/java/editor/src/main/java/com/android/apksig/apk/CodenameMinSdkVersionException.java new file mode 100644 index 0000000000..e30bc359a6 --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/apk/CodenameMinSdkVersionException.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.apk; + +/** + * Indicates that there was an issue determining the minimum Android platform version supported by + * an APK because the version is specified as a codename, rather than as API Level number, and the + * codename is in an unexpected format. + */ +public class CodenameMinSdkVersionException extends MinSdkVersionException { + + private static final long serialVersionUID = 1L; + + /** Encountered codename. */ + private final String mCodename; + + /** + * Constructs a new {@code MinSdkVersionCodenameException} with the provided message and + * codename. + */ + public CodenameMinSdkVersionException(String message, String codename) { + super(message); + mCodename = codename; + } + + /** + * Returns the codename. + */ + public String getCodename() { + return mCodename; + } +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/apk/MinSdkVersionException.java b/platform/android/java/editor/src/main/java/com/android/apksig/apk/MinSdkVersionException.java new file mode 100644 index 0000000000..c4aad08067 --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/apk/MinSdkVersionException.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.apk; + +/** + * Indicates that there was an issue determining the minimum Android platform version supported by + * an APK. + */ +public class MinSdkVersionException extends ApkFormatException { + + private static final long serialVersionUID = 1L; + + /** + * Constructs a new {@code MinSdkVersionException} with the provided message. + */ + public MinSdkVersionException(String message) { + super(message); + } + + /** + * Constructs a new {@code MinSdkVersionException} with the provided message and cause. + */ + public MinSdkVersionException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/AndroidBinXmlParser.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/AndroidBinXmlParser.java new file mode 100644 index 0000000000..bc5a45738b --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/AndroidBinXmlParser.java @@ -0,0 +1,869 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.apk; + +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * XML pull style parser of Android binary XML resources, such as {@code AndroidManifest.xml}. + * + * <p>For an input document, the parser outputs an event stream (see {@code EVENT_... constants} via + * {@link #getEventType()} and {@link #next()} methods. Additional information about the current + * event can be obtained via an assortment of getters, for example, {@link #getName()} or + * {@link #getAttributeNameResourceId(int)}. + */ +public class AndroidBinXmlParser { + + /** Event: start of document. */ + public static final int EVENT_START_DOCUMENT = 1; + + /** Event: end of document. */ + public static final int EVENT_END_DOCUMENT = 2; + + /** Event: start of an element. */ + public static final int EVENT_START_ELEMENT = 3; + + /** Event: end of an document. */ + public static final int EVENT_END_ELEMENT = 4; + + /** Attribute value type is not supported by this parser. */ + public static final int VALUE_TYPE_UNSUPPORTED = 0; + + /** Attribute value is a string. Use {@link #getAttributeStringValue(int)} to obtain it. */ + public static final int VALUE_TYPE_STRING = 1; + + /** Attribute value is an integer. Use {@link #getAttributeIntValue(int)} to obtain it. */ + public static final int VALUE_TYPE_INT = 2; + + /** + * Attribute value is a resource reference. Use {@link #getAttributeIntValue(int)} to obtain it. + */ + public static final int VALUE_TYPE_REFERENCE = 3; + + /** Attribute value is a boolean. Use {@link #getAttributeBooleanValue(int)} to obtain it. */ + public static final int VALUE_TYPE_BOOLEAN = 4; + + private static final long NO_NAMESPACE = 0xffffffffL; + + private final ByteBuffer mXml; + + private StringPool mStringPool; + private ResourceMap mResourceMap; + private int mDepth; + private int mCurrentEvent = EVENT_START_DOCUMENT; + + private String mCurrentElementName; + private String mCurrentElementNamespace; + private int mCurrentElementAttributeCount; + private List<Attribute> mCurrentElementAttributes; + private ByteBuffer mCurrentElementAttributesContents; + private int mCurrentElementAttrSizeBytes; + + /** + * Constructs a new parser for the provided document. + */ + public AndroidBinXmlParser(ByteBuffer xml) throws XmlParserException { + xml.order(ByteOrder.LITTLE_ENDIAN); + + Chunk resXmlChunk = null; + while (xml.hasRemaining()) { + Chunk chunk = Chunk.get(xml); + if (chunk == null) { + break; + } + if (chunk.getType() == Chunk.TYPE_RES_XML) { + resXmlChunk = chunk; + break; + } + } + + if (resXmlChunk == null) { + throw new XmlParserException("No XML chunk in file"); + } + mXml = resXmlChunk.getContents(); + } + + /** + * Returns the depth of the current element. Outside of the root of the document the depth is + * {@code 0}. The depth is incremented by {@code 1} before each {@code start element} event and + * is decremented by {@code 1} after each {@code end element} event. + */ + public int getDepth() { + return mDepth; + } + + /** + * Returns the type of the current event. See {@code EVENT_...} constants. + */ + public int getEventType() { + return mCurrentEvent; + } + + /** + * Returns the local name of the current element or {@code null} if the current event does not + * pertain to an element. + */ + public String getName() { + if ((mCurrentEvent != EVENT_START_ELEMENT) && (mCurrentEvent != EVENT_END_ELEMENT)) { + return null; + } + return mCurrentElementName; + } + + /** + * Returns the namespace of the current element or {@code null} if the current event does not + * pertain to an element. Returns an empty string if the element is not associated with a + * namespace. + */ + public String getNamespace() { + if ((mCurrentEvent != EVENT_START_ELEMENT) && (mCurrentEvent != EVENT_END_ELEMENT)) { + return null; + } + return mCurrentElementNamespace; + } + + /** + * Returns the number of attributes of the element associated with the current event or + * {@code -1} if no element is associated with the current event. + */ + public int getAttributeCount() { + if (mCurrentEvent != EVENT_START_ELEMENT) { + return -1; + } + + return mCurrentElementAttributeCount; + } + + /** + * Returns the resource ID corresponding to the name of the specified attribute of the current + * element or {@code 0} if the name is not associated with a resource ID. + * + * @throws IndexOutOfBoundsException if the index is out of range or the current event is not a + * {@code start element} event + * @throws XmlParserException if a parsing error is occurred + */ + public int getAttributeNameResourceId(int index) throws XmlParserException { + return getAttribute(index).getNameResourceId(); + } + + /** + * Returns the name of the specified attribute of the current element. + * + * @throws IndexOutOfBoundsException if the index is out of range or the current event is not a + * {@code start element} event + * @throws XmlParserException if a parsing error is occurred + */ + public String getAttributeName(int index) throws XmlParserException { + return getAttribute(index).getName(); + } + + /** + * Returns the name of the specified attribute of the current element or an empty string if + * the attribute is not associated with a namespace. + * + * @throws IndexOutOfBoundsException if the index is out of range or the current event is not a + * {@code start element} event + * @throws XmlParserException if a parsing error is occurred + */ + public String getAttributeNamespace(int index) throws XmlParserException { + return getAttribute(index).getNamespace(); + } + + /** + * Returns the value type of the specified attribute of the current element. See + * {@code VALUE_TYPE_...} constants. + * + * @throws IndexOutOfBoundsException if the index is out of range or the current event is not a + * {@code start element} event + * @throws XmlParserException if a parsing error is occurred + */ + public int getAttributeValueType(int index) throws XmlParserException { + int type = getAttribute(index).getValueType(); + switch (type) { + case Attribute.TYPE_STRING: + return VALUE_TYPE_STRING; + case Attribute.TYPE_INT_DEC: + case Attribute.TYPE_INT_HEX: + return VALUE_TYPE_INT; + case Attribute.TYPE_REFERENCE: + return VALUE_TYPE_REFERENCE; + case Attribute.TYPE_INT_BOOLEAN: + return VALUE_TYPE_BOOLEAN; + default: + return VALUE_TYPE_UNSUPPORTED; + } + } + + /** + * Returns the integer value of the specified attribute of the current element. See + * {@code VALUE_TYPE_...} constants. + * + * @throws IndexOutOfBoundsException if the index is out of range or the current event is not a + * {@code start element} event. + * @throws XmlParserException if a parsing error is occurred + */ + public int getAttributeIntValue(int index) throws XmlParserException { + return getAttribute(index).getIntValue(); + } + + /** + * Returns the boolean value of the specified attribute of the current element. See + * {@code VALUE_TYPE_...} constants. + * + * @throws IndexOutOfBoundsException if the index is out of range or the current event is not a + * {@code start element} event. + * @throws XmlParserException if a parsing error is occurred + */ + public boolean getAttributeBooleanValue(int index) throws XmlParserException { + return getAttribute(index).getBooleanValue(); + } + + /** + * Returns the string value of the specified attribute of the current element. See + * {@code VALUE_TYPE_...} constants. + * + * @throws IndexOutOfBoundsException if the index is out of range or the current event is not a + * {@code start element} event. + * @throws XmlParserException if a parsing error is occurred + */ + public String getAttributeStringValue(int index) throws XmlParserException { + return getAttribute(index).getStringValue(); + } + + private Attribute getAttribute(int index) { + if (mCurrentEvent != EVENT_START_ELEMENT) { + throw new IndexOutOfBoundsException("Current event not a START_ELEMENT"); + } + if (index < 0) { + throw new IndexOutOfBoundsException("index must be >= 0"); + } + if (index >= mCurrentElementAttributeCount) { + throw new IndexOutOfBoundsException( + "index must be <= attr count (" + mCurrentElementAttributeCount + ")"); + } + parseCurrentElementAttributesIfNotParsed(); + return mCurrentElementAttributes.get(index); + } + + /** + * Advances to the next parsing event and returns its type. See {@code EVENT_...} constants. + */ + public int next() throws XmlParserException { + // Decrement depth if the previous event was "end element". + if (mCurrentEvent == EVENT_END_ELEMENT) { + mDepth--; + } + + // Read events from document, ignoring events that we don't report to caller. Stop at the + // earliest event which we report to caller. + while (mXml.hasRemaining()) { + Chunk chunk = Chunk.get(mXml); + if (chunk == null) { + break; + } + switch (chunk.getType()) { + case Chunk.TYPE_STRING_POOL: + if (mStringPool != null) { + throw new XmlParserException("Multiple string pools not supported"); + } + mStringPool = new StringPool(chunk); + break; + + case Chunk.RES_XML_TYPE_START_ELEMENT: + { + if (mStringPool == null) { + throw new XmlParserException( + "Named element encountered before string pool"); + } + ByteBuffer contents = chunk.getContents(); + if (contents.remaining() < 20) { + throw new XmlParserException( + "Start element chunk too short. Need at least 20 bytes. Available: " + + contents.remaining() + " bytes"); + } + long nsId = getUnsignedInt32(contents); + long nameId = getUnsignedInt32(contents); + int attrStartOffset = getUnsignedInt16(contents); + int attrSizeBytes = getUnsignedInt16(contents); + int attrCount = getUnsignedInt16(contents); + long attrEndOffset = attrStartOffset + ((long) attrCount) * attrSizeBytes; + contents.position(0); + if (attrStartOffset > contents.remaining()) { + throw new XmlParserException( + "Attributes start offset out of bounds: " + attrStartOffset + + ", max: " + contents.remaining()); + } + if (attrEndOffset > contents.remaining()) { + throw new XmlParserException( + "Attributes end offset out of bounds: " + attrEndOffset + + ", max: " + contents.remaining()); + } + + mCurrentElementName = mStringPool.getString(nameId); + mCurrentElementNamespace = + (nsId == NO_NAMESPACE) ? "" : mStringPool.getString(nsId); + mCurrentElementAttributeCount = attrCount; + mCurrentElementAttributes = null; + mCurrentElementAttrSizeBytes = attrSizeBytes; + mCurrentElementAttributesContents = + sliceFromTo(contents, attrStartOffset, attrEndOffset); + + mDepth++; + mCurrentEvent = EVENT_START_ELEMENT; + return mCurrentEvent; + } + + case Chunk.RES_XML_TYPE_END_ELEMENT: + { + if (mStringPool == null) { + throw new XmlParserException( + "Named element encountered before string pool"); + } + ByteBuffer contents = chunk.getContents(); + if (contents.remaining() < 8) { + throw new XmlParserException( + "End element chunk too short. Need at least 8 bytes. Available: " + + contents.remaining() + " bytes"); + } + long nsId = getUnsignedInt32(contents); + long nameId = getUnsignedInt32(contents); + mCurrentElementName = mStringPool.getString(nameId); + mCurrentElementNamespace = + (nsId == NO_NAMESPACE) ? "" : mStringPool.getString(nsId); + mCurrentEvent = EVENT_END_ELEMENT; + mCurrentElementAttributes = null; + mCurrentElementAttributesContents = null; + return mCurrentEvent; + } + case Chunk.RES_XML_TYPE_RESOURCE_MAP: + if (mResourceMap != null) { + throw new XmlParserException("Multiple resource maps not supported"); + } + mResourceMap = new ResourceMap(chunk); + break; + default: + // Unknown chunk type -- ignore + break; + } + } + + mCurrentEvent = EVENT_END_DOCUMENT; + return mCurrentEvent; + } + + private void parseCurrentElementAttributesIfNotParsed() { + if (mCurrentElementAttributes != null) { + return; + } + mCurrentElementAttributes = new ArrayList<>(mCurrentElementAttributeCount); + for (int i = 0; i < mCurrentElementAttributeCount; i++) { + int startPosition = i * mCurrentElementAttrSizeBytes; + ByteBuffer attr = + sliceFromTo( + mCurrentElementAttributesContents, + startPosition, + startPosition + mCurrentElementAttrSizeBytes); + long nsId = getUnsignedInt32(attr); + long nameId = getUnsignedInt32(attr); + attr.position(attr.position() + 7); // skip ignored fields + int valueType = getUnsignedInt8(attr); + long valueData = getUnsignedInt32(attr); + mCurrentElementAttributes.add( + new Attribute( + nsId, + nameId, + valueType, + (int) valueData, + mStringPool, + mResourceMap)); + } + } + + private static class Attribute { + private static final int TYPE_REFERENCE = 1; + private static final int TYPE_STRING = 3; + private static final int TYPE_INT_DEC = 0x10; + private static final int TYPE_INT_HEX = 0x11; + private static final int TYPE_INT_BOOLEAN = 0x12; + + private final long mNsId; + private final long mNameId; + private final int mValueType; + private final int mValueData; + private final StringPool mStringPool; + private final ResourceMap mResourceMap; + + private Attribute( + long nsId, + long nameId, + int valueType, + int valueData, + StringPool stringPool, + ResourceMap resourceMap) { + mNsId = nsId; + mNameId = nameId; + mValueType = valueType; + mValueData = valueData; + mStringPool = stringPool; + mResourceMap = resourceMap; + } + + public int getNameResourceId() { + return (mResourceMap != null) ? mResourceMap.getResourceId(mNameId) : 0; + } + + public String getName() throws XmlParserException { + return mStringPool.getString(mNameId); + } + + public String getNamespace() throws XmlParserException { + return (mNsId != NO_NAMESPACE) ? mStringPool.getString(mNsId) : ""; + } + + public int getValueType() { + return mValueType; + } + + public int getIntValue() throws XmlParserException { + switch (mValueType) { + case TYPE_REFERENCE: + case TYPE_INT_DEC: + case TYPE_INT_HEX: + case TYPE_INT_BOOLEAN: + return mValueData; + default: + throw new XmlParserException("Cannot coerce to int: value type " + mValueType); + } + } + + public boolean getBooleanValue() throws XmlParserException { + switch (mValueType) { + case TYPE_INT_BOOLEAN: + return mValueData != 0; + default: + throw new XmlParserException( + "Cannot coerce to boolean: value type " + mValueType); + } + } + + public String getStringValue() throws XmlParserException { + switch (mValueType) { + case TYPE_STRING: + return mStringPool.getString(mValueData & 0xffffffffL); + case TYPE_INT_DEC: + return Integer.toString(mValueData); + case TYPE_INT_HEX: + return "0x" + Integer.toHexString(mValueData); + case TYPE_INT_BOOLEAN: + return Boolean.toString(mValueData != 0); + case TYPE_REFERENCE: + return "@" + Integer.toHexString(mValueData); + default: + throw new XmlParserException( + "Cannot coerce to string: value type " + mValueType); + } + } + } + + /** + * Chunk of a document. Each chunk is tagged with a type and consists of a header followed by + * contents. + */ + private static class Chunk { + public static final int TYPE_STRING_POOL = 1; + public static final int TYPE_RES_XML = 3; + public static final int RES_XML_TYPE_START_ELEMENT = 0x0102; + public static final int RES_XML_TYPE_END_ELEMENT = 0x0103; + public static final int RES_XML_TYPE_RESOURCE_MAP = 0x0180; + + static final int HEADER_MIN_SIZE_BYTES = 8; + + private final int mType; + private final ByteBuffer mHeader; + private final ByteBuffer mContents; + + public Chunk(int type, ByteBuffer header, ByteBuffer contents) { + mType = type; + mHeader = header; + mContents = contents; + } + + public ByteBuffer getContents() { + ByteBuffer result = mContents.slice(); + result.order(mContents.order()); + return result; + } + + public ByteBuffer getHeader() { + ByteBuffer result = mHeader.slice(); + result.order(mHeader.order()); + return result; + } + + public int getType() { + return mType; + } + + /** + * Consumes the chunk located at the current position of the input and returns the chunk + * or {@code null} if there is no chunk left in the input. + * + * @throws XmlParserException if the chunk is malformed + */ + public static Chunk get(ByteBuffer input) throws XmlParserException { + if (input.remaining() < HEADER_MIN_SIZE_BYTES) { + // Android ignores the last chunk if its header is too big to fit into the file + input.position(input.limit()); + return null; + } + + int originalPosition = input.position(); + int type = getUnsignedInt16(input); + int headerSize = getUnsignedInt16(input); + long chunkSize = getUnsignedInt32(input); + long chunkRemaining = chunkSize - 8; + if (chunkRemaining > input.remaining()) { + // Android ignores the last chunk if it's too big to fit into the file + input.position(input.limit()); + return null; + } + if (headerSize < HEADER_MIN_SIZE_BYTES) { + throw new XmlParserException( + "Malformed chunk: header too short: " + headerSize + " bytes"); + } else if (headerSize > chunkSize) { + throw new XmlParserException( + "Malformed chunk: header too long: " + headerSize + " bytes. Chunk size: " + + chunkSize + " bytes"); + } + int contentStartPosition = originalPosition + headerSize; + long chunkEndPosition = originalPosition + chunkSize; + Chunk chunk = + new Chunk( + type, + sliceFromTo(input, originalPosition, contentStartPosition), + sliceFromTo(input, contentStartPosition, chunkEndPosition)); + input.position((int) chunkEndPosition); + return chunk; + } + } + + /** + * String pool of a document. Strings are referenced by their {@code 0}-based index in the pool. + */ + private static class StringPool { + private static final int FLAG_UTF8 = 1 << 8; + + private final ByteBuffer mChunkContents; + private final ByteBuffer mStringsSection; + private final int mStringCount; + private final boolean mUtf8Encoded; + private final Map<Integer, String> mCachedStrings = new HashMap<>(); + + /** + * Constructs a new string pool from the provided chunk. + * + * @throws XmlParserException if a parsing error occurred + */ + public StringPool(Chunk chunk) throws XmlParserException { + ByteBuffer header = chunk.getHeader(); + int headerSizeBytes = header.remaining(); + header.position(Chunk.HEADER_MIN_SIZE_BYTES); + if (header.remaining() < 20) { + throw new XmlParserException( + "XML chunk's header too short. Required at least 20 bytes. Available: " + + header.remaining() + " bytes"); + } + long stringCount = getUnsignedInt32(header); + if (stringCount > Integer.MAX_VALUE) { + throw new XmlParserException("Too many strings: " + stringCount); + } + mStringCount = (int) stringCount; + long styleCount = getUnsignedInt32(header); + if (styleCount > Integer.MAX_VALUE) { + throw new XmlParserException("Too many styles: " + styleCount); + } + long flags = getUnsignedInt32(header); + long stringsStartOffset = getUnsignedInt32(header); + long stylesStartOffset = getUnsignedInt32(header); + + ByteBuffer contents = chunk.getContents(); + if (mStringCount > 0) { + int stringsSectionStartOffsetInContents = + (int) (stringsStartOffset - headerSizeBytes); + int stringsSectionEndOffsetInContents; + if (styleCount > 0) { + // Styles section follows the strings section + if (stylesStartOffset < stringsStartOffset) { + throw new XmlParserException( + "Styles offset (" + stylesStartOffset + ") < strings offset (" + + stringsStartOffset + ")"); + } + stringsSectionEndOffsetInContents = (int) (stylesStartOffset - headerSizeBytes); + } else { + stringsSectionEndOffsetInContents = contents.remaining(); + } + mStringsSection = + sliceFromTo( + contents, + stringsSectionStartOffsetInContents, + stringsSectionEndOffsetInContents); + } else { + mStringsSection = ByteBuffer.allocate(0); + } + + mUtf8Encoded = (flags & FLAG_UTF8) != 0; + mChunkContents = contents; + } + + /** + * Returns the string located at the specified {@code 0}-based index in this pool. + * + * @throws XmlParserException if the string does not exist or cannot be decoded + */ + public String getString(long index) throws XmlParserException { + if (index < 0) { + throw new XmlParserException("Unsuported string index: " + index); + } else if (index >= mStringCount) { + throw new XmlParserException( + "Unsuported string index: " + index + ", max: " + (mStringCount - 1)); + } + + int idx = (int) index; + String result = mCachedStrings.get(idx); + if (result != null) { + return result; + } + + long offsetInStringsSection = getUnsignedInt32(mChunkContents, idx * 4); + if (offsetInStringsSection >= mStringsSection.capacity()) { + throw new XmlParserException( + "Offset of string idx " + idx + " out of bounds: " + offsetInStringsSection + + ", max: " + (mStringsSection.capacity() - 1)); + } + mStringsSection.position((int) offsetInStringsSection); + result = + (mUtf8Encoded) + ? getLengthPrefixedUtf8EncodedString(mStringsSection) + : getLengthPrefixedUtf16EncodedString(mStringsSection); + mCachedStrings.put(idx, result); + return result; + } + + private static String getLengthPrefixedUtf16EncodedString(ByteBuffer encoded) + throws XmlParserException { + // If the length (in uint16s) is 0x7fff or lower, it is stored as a single uint16. + // Otherwise, it is stored as a big-endian uint32 with highest bit set. Thus, the range + // of supported values is 0 to 0x7fffffff inclusive. + int lengthChars = getUnsignedInt16(encoded); + if ((lengthChars & 0x8000) != 0) { + lengthChars = ((lengthChars & 0x7fff) << 16) | getUnsignedInt16(encoded); + } + if (lengthChars > Integer.MAX_VALUE / 2) { + throw new XmlParserException("String too long: " + lengthChars + " uint16s"); + } + int lengthBytes = lengthChars * 2; + + byte[] arr; + int arrOffset; + if (encoded.hasArray()) { + arr = encoded.array(); + arrOffset = encoded.arrayOffset() + encoded.position(); + encoded.position(encoded.position() + lengthBytes); + } else { + arr = new byte[lengthBytes]; + arrOffset = 0; + encoded.get(arr); + } + // Reproduce the behavior of Android runtime which requires that the UTF-16 encoded + // array of bytes is NULL terminated. + if ((arr[arrOffset + lengthBytes] != 0) + || (arr[arrOffset + lengthBytes + 1] != 0)) { + throw new XmlParserException("UTF-16 encoded form of string not NULL terminated"); + } + try { + return new String(arr, arrOffset, lengthBytes, "UTF-16LE"); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException("UTF-16LE character encoding not supported", e); + } + } + + private static String getLengthPrefixedUtf8EncodedString(ByteBuffer encoded) + throws XmlParserException { + // If the length (in bytes) is 0x7f or lower, it is stored as a single uint8. Otherwise, + // it is stored as a big-endian uint16 with highest bit set. Thus, the range of + // supported values is 0 to 0x7fff inclusive. + + // Skip UTF-16 encoded length (in uint16s) + int lengthBytes = getUnsignedInt8(encoded); + if ((lengthBytes & 0x80) != 0) { + lengthBytes = ((lengthBytes & 0x7f) << 8) | getUnsignedInt8(encoded); + } + + // Read UTF-8 encoded length (in bytes) + lengthBytes = getUnsignedInt8(encoded); + if ((lengthBytes & 0x80) != 0) { + lengthBytes = ((lengthBytes & 0x7f) << 8) | getUnsignedInt8(encoded); + } + + byte[] arr; + int arrOffset; + if (encoded.hasArray()) { + arr = encoded.array(); + arrOffset = encoded.arrayOffset() + encoded.position(); + encoded.position(encoded.position() + lengthBytes); + } else { + arr = new byte[lengthBytes]; + arrOffset = 0; + encoded.get(arr); + } + // Reproduce the behavior of Android runtime which requires that the UTF-8 encoded array + // of bytes is NULL terminated. + if (arr[arrOffset + lengthBytes] != 0) { + throw new XmlParserException("UTF-8 encoded form of string not NULL terminated"); + } + try { + return new String(arr, arrOffset, lengthBytes, "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException("UTF-8 character encoding not supported", e); + } + } + } + + /** + * Resource map of a document. Resource IDs are referenced by their {@code 0}-based index in the + * map. + */ + private static class ResourceMap { + private final ByteBuffer mChunkContents; + private final int mEntryCount; + + /** + * Constructs a new resource map from the provided chunk. + * + * @throws XmlParserException if a parsing error occurred + */ + public ResourceMap(Chunk chunk) throws XmlParserException { + mChunkContents = chunk.getContents().slice(); + mChunkContents.order(chunk.getContents().order()); + // Each entry of the map is four bytes long, containing the int32 resource ID. + mEntryCount = mChunkContents.remaining() / 4; + } + + /** + * Returns the resource ID located at the specified {@code 0}-based index in this pool or + * {@code 0} if the index is out of range. + */ + public int getResourceId(long index) { + if ((index < 0) || (index >= mEntryCount)) { + return 0; + } + int idx = (int) index; + // Each entry of the map is four bytes long, containing the int32 resource ID. + return mChunkContents.getInt(idx * 4); + } + } + + /** + * Returns new byte buffer whose content is a shared subsequence of this buffer's content + * between the specified start (inclusive) and end (exclusive) positions. As opposed to + * {@link ByteBuffer#slice()}, the returned buffer's byte order is the same as the source + * buffer's byte order. + */ + private static ByteBuffer sliceFromTo(ByteBuffer source, long start, long end) { + if (start < 0) { + throw new IllegalArgumentException("start: " + start); + } + if (end < start) { + throw new IllegalArgumentException("end < start: " + end + " < " + start); + } + int capacity = source.capacity(); + if (end > source.capacity()) { + throw new IllegalArgumentException("end > capacity: " + end + " > " + capacity); + } + return sliceFromTo(source, (int) start, (int) end); + } + + /** + * Returns new byte buffer whose content is a shared subsequence of this buffer's content + * between the specified start (inclusive) and end (exclusive) positions. As opposed to + * {@link ByteBuffer#slice()}, the returned buffer's byte order is the same as the source + * buffer's byte order. + */ + private static ByteBuffer sliceFromTo(ByteBuffer source, int start, int end) { + if (start < 0) { + throw new IllegalArgumentException("start: " + start); + } + if (end < start) { + throw new IllegalArgumentException("end < start: " + end + " < " + start); + } + int capacity = source.capacity(); + if (end > source.capacity()) { + throw new IllegalArgumentException("end > capacity: " + end + " > " + capacity); + } + int originalLimit = source.limit(); + int originalPosition = source.position(); + try { + source.position(0); + source.limit(end); + source.position(start); + ByteBuffer result = source.slice(); + result.order(source.order()); + return result; + } finally { + source.position(0); + source.limit(originalLimit); + source.position(originalPosition); + } + } + + private static int getUnsignedInt8(ByteBuffer buffer) { + return buffer.get() & 0xff; + } + + private static int getUnsignedInt16(ByteBuffer buffer) { + return buffer.getShort() & 0xffff; + } + + private static long getUnsignedInt32(ByteBuffer buffer) { + return buffer.getInt() & 0xffffffffL; + } + + private static long getUnsignedInt32(ByteBuffer buffer, int position) { + return buffer.getInt(position) & 0xffffffffL; + } + + /** + * Indicates that an error occurred while parsing a document. + */ + public static class XmlParserException extends Exception { + private static final long serialVersionUID = 1L; + + public XmlParserException(String message) { + super(message); + } + + public XmlParserException(String message, Throwable cause) { + super(message, cause); + } + } +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/ApkSigResult.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/ApkSigResult.java new file mode 100644 index 0000000000..6151351b2b --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/ApkSigResult.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.apk; + +import com.android.apksig.ApkVerificationIssue; + +import java.util.ArrayList; +import java.util.List; + +/** + * Base implementation of an APK signature verification result. + */ +public class ApkSigResult { + public final int signatureSchemeVersion; + + /** Whether the APK's Signature Scheme signature verifies. */ + public boolean verified; + + public final List<ApkSignerInfo> mSigners = new ArrayList<>(); + private final List<ApkVerificationIssue> mWarnings = new ArrayList<>(); + private final List<ApkVerificationIssue> mErrors = new ArrayList<>(); + + public ApkSigResult(int signatureSchemeVersion) { + this.signatureSchemeVersion = signatureSchemeVersion; + } + + /** + * Returns {@code true} if this result encountered errors during verification. + */ + public boolean containsErrors() { + if (!mErrors.isEmpty()) { + return true; + } + if (!mSigners.isEmpty()) { + for (ApkSignerInfo signer : mSigners) { + if (signer.containsErrors()) { + return true; + } + } + } + return false; + } + + /** + * Returns {@code true} if this result encountered warnings during verification. + */ + public boolean containsWarnings() { + if (!mWarnings.isEmpty()) { + return true; + } + if (!mSigners.isEmpty()) { + for (ApkSignerInfo signer : mSigners) { + if (signer.containsWarnings()) { + return true; + } + } + } + return false; + } + + /** + * Adds a new {@link ApkVerificationIssue} as an error to this result using the provided {@code + * issueId} and {@code params}. + */ + public void addError(int issueId, Object... parameters) { + mErrors.add(new ApkVerificationIssue(issueId, parameters)); + } + + /** + * Adds a new {@link ApkVerificationIssue} as a warning to this result using the provided {@code + * issueId} and {@code params}. + */ + public void addWarning(int issueId, Object... parameters) { + mWarnings.add(new ApkVerificationIssue(issueId, parameters)); + } + + /** + * Returns the errors encountered during verification. + */ + public List<? extends ApkVerificationIssue> getErrors() { + return mErrors; + } + + /** + * Returns the warnings encountered during verification. + */ + public List<? extends ApkVerificationIssue> getWarnings() { + return mWarnings; + } +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/ApkSignerInfo.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/ApkSignerInfo.java new file mode 100644 index 0000000000..3e7934195f --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/ApkSignerInfo.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.apk; + +import com.android.apksig.ApkVerificationIssue; + +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.List; + +/** + * Base implementation of an APK signer. + */ +public class ApkSignerInfo { + public int index; + public long timestamp; + public List<X509Certificate> certs = new ArrayList<>(); + public List<X509Certificate> certificateLineage = new ArrayList<>(); + + private final List<ApkVerificationIssue> mInfoMessages = new ArrayList<>(); + private final List<ApkVerificationIssue> mWarnings = new ArrayList<>(); + private final List<ApkVerificationIssue> mErrors = new ArrayList<>(); + + /** + * Adds a new {@link ApkVerificationIssue} as an error to this signer using the provided {@code + * issueId} and {@code params}. + */ + public void addError(int issueId, Object... params) { + mErrors.add(new ApkVerificationIssue(issueId, params)); + } + + /** + * Adds a new {@link ApkVerificationIssue} as a warning to this signer using the provided {@code + * issueId} and {@code params}. + */ + public void addWarning(int issueId, Object... params) { + mWarnings.add(new ApkVerificationIssue(issueId, params)); + } + + /** + * Adds a new {@link ApkVerificationIssue} as an info message to this signer config using the + * provided {@code issueId} and {@code params}. + */ + public void addInfoMessage(int issueId, Object... params) { + mInfoMessages.add(new ApkVerificationIssue(issueId, params)); + } + + /** + * Returns {@code true} if any errors were encountered during verification for this signer. + */ + public boolean containsErrors() { + return !mErrors.isEmpty(); + } + + /** + * Returns {@code true} if any warnings were encountered during verification for this signer. + */ + public boolean containsWarnings() { + return !mWarnings.isEmpty(); + } + + /** + * Returns {@code true} if any info messages were encountered during verification of this + * signer. + */ + public boolean containsInfoMessages() { + return !mInfoMessages.isEmpty(); + } + + /** + * Returns the errors encountered during verification for this signer. + */ + public List<? extends ApkVerificationIssue> getErrors() { + return mErrors; + } + + /** + * Returns the warnings encountered during verification for this signer. + */ + public List<? extends ApkVerificationIssue> getWarnings() { + return mWarnings; + } + + /** + * Returns the info messages encountered during verification of this signer. + */ + public List<? extends ApkVerificationIssue> getInfoMessages() { + return mInfoMessages; + } +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtils.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtils.java new file mode 100644 index 0000000000..127ac24d3f --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtils.java @@ -0,0 +1,1444 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.apk; + +import static com.android.apksig.Constants.OID_RSA_ENCRYPTION; +import static com.android.apksig.internal.apk.ContentDigestAlgorithm.CHUNKED_SHA256; +import static com.android.apksig.internal.apk.ContentDigestAlgorithm.CHUNKED_SHA512; +import static com.android.apksig.internal.apk.ContentDigestAlgorithm.VERITY_CHUNKED_SHA256; + +import com.android.apksig.ApkVerifier; +import com.android.apksig.SigningCertificateLineage; +import com.android.apksig.apk.ApkFormatException; +import com.android.apksig.apk.ApkUtils; +import com.android.apksig.internal.asn1.Asn1BerParser; +import com.android.apksig.internal.asn1.Asn1DecodingException; +import com.android.apksig.internal.asn1.Asn1DerEncoder; +import com.android.apksig.internal.asn1.Asn1EncodingException; +import com.android.apksig.internal.asn1.Asn1OpaqueObject; +import com.android.apksig.internal.pkcs7.AlgorithmIdentifier; +import com.android.apksig.internal.pkcs7.ContentInfo; +import com.android.apksig.internal.pkcs7.EncapsulatedContentInfo; +import com.android.apksig.internal.pkcs7.IssuerAndSerialNumber; +import com.android.apksig.internal.pkcs7.Pkcs7Constants; +import com.android.apksig.internal.pkcs7.SignedData; +import com.android.apksig.internal.pkcs7.SignerIdentifier; +import com.android.apksig.internal.pkcs7.SignerInfo; +import com.android.apksig.internal.util.ByteBufferDataSource; +import com.android.apksig.internal.util.ChainedDataSource; +import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate; +import com.android.apksig.internal.util.Pair; +import com.android.apksig.internal.util.VerityTreeBuilder; +import com.android.apksig.internal.util.X509CertificateUtils; +import com.android.apksig.internal.x509.RSAPublicKey; +import com.android.apksig.internal.x509.SubjectPublicKeyInfo; +import com.android.apksig.internal.zip.ZipUtils; +import com.android.apksig.util.DataSink; +import com.android.apksig.util.DataSinks; +import com.android.apksig.util.DataSource; +import com.android.apksig.util.DataSources; +import com.android.apksig.util.RunnablesExecutor; + +import java.io.IOException; +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.security.DigestException; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.KeyFactory; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.Signature; +import java.security.SignatureException; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.security.spec.AlgorithmParameterSpec; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.X509EncodedKeySpec; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Supplier; + +import javax.security.auth.x500.X500Principal; + +public class ApkSigningBlockUtils { + + private static final long CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES = 1024 * 1024; + public static final int ANDROID_COMMON_PAGE_ALIGNMENT_BYTES = 4096; + private static final byte[] APK_SIGNING_BLOCK_MAGIC = + new byte[] { + 0x41, 0x50, 0x4b, 0x20, 0x53, 0x69, 0x67, 0x20, + 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x20, 0x34, 0x32, + }; + public static final int VERITY_PADDING_BLOCK_ID = 0x42726577; + + private static final ContentDigestAlgorithm[] V4_CONTENT_DIGEST_ALGORITHMS = + {CHUNKED_SHA512, VERITY_CHUNKED_SHA256, CHUNKED_SHA256}; + + public static final int VERSION_SOURCE_STAMP = 0; + public static final int VERSION_JAR_SIGNATURE_SCHEME = 1; + public static final int VERSION_APK_SIGNATURE_SCHEME_V2 = 2; + public static final int VERSION_APK_SIGNATURE_SCHEME_V3 = 3; + public static final int VERSION_APK_SIGNATURE_SCHEME_V31 = 31; + public static final int VERSION_APK_SIGNATURE_SCHEME_V4 = 4; + + /** + * Returns positive number if {@code alg1} is preferred over {@code alg2}, {@code -1} if + * {@code alg2} is preferred over {@code alg1}, and {@code 0} if there is no preference. + */ + public static int compareSignatureAlgorithm(SignatureAlgorithm alg1, SignatureAlgorithm alg2) { + return ApkSigningBlockUtilsLite.compareSignatureAlgorithm(alg1, alg2); + } + + /** + * Verifies integrity of the APK outside of the APK Signing Block by computing digests of the + * APK and comparing them against the digests listed in APK Signing Block. The expected digests + * are taken from {@code SignerInfos} of the provided {@code result}. + * + * <p>This method adds one or more errors to the {@code result} if a verification error is + * expected to be encountered on Android. No errors are added to the {@code result} if the APK's + * integrity is expected to verify on Android for each algorithm in + * {@code contentDigestAlgorithms}. + * + * <p>The reason this method is currently not parameterized by a + * {@code [minSdkVersion, maxSdkVersion]} range is that up until now content digest algorithms + * exhibit the same behavior on all Android platform versions. + */ + public static void verifyIntegrity( + RunnablesExecutor executor, + DataSource beforeApkSigningBlock, + DataSource centralDir, + ByteBuffer eocd, + Set<ContentDigestAlgorithm> contentDigestAlgorithms, + Result result) throws IOException, NoSuchAlgorithmException { + if (contentDigestAlgorithms.isEmpty()) { + // This should never occur because this method is invoked once at least one signature + // is verified, meaning at least one content digest is known. + throw new RuntimeException("No content digests found"); + } + + // For the purposes of verifying integrity, ZIP End of Central Directory (EoCD) must be + // treated as though its Central Directory offset points to the start of APK Signing Block. + // We thus modify the EoCD accordingly. + ByteBuffer modifiedEocd = ByteBuffer.allocate(eocd.remaining()); + int eocdSavedPos = eocd.position(); + modifiedEocd.order(ByteOrder.LITTLE_ENDIAN); + modifiedEocd.put(eocd); + modifiedEocd.flip(); + + // restore eocd to position prior to modification in case it is to be used elsewhere + eocd.position(eocdSavedPos); + ZipUtils.setZipEocdCentralDirectoryOffset(modifiedEocd, beforeApkSigningBlock.size()); + Map<ContentDigestAlgorithm, byte[]> actualContentDigests; + try { + actualContentDigests = + computeContentDigests( + executor, + contentDigestAlgorithms, + beforeApkSigningBlock, + centralDir, + new ByteBufferDataSource(modifiedEocd)); + // Special checks for the verity algorithm requirements. + if (actualContentDigests.containsKey(VERITY_CHUNKED_SHA256)) { + if ((beforeApkSigningBlock.size() % ANDROID_COMMON_PAGE_ALIGNMENT_BYTES != 0)) { + throw new RuntimeException( + "APK Signing Block is not aligned on 4k boundary: " + + beforeApkSigningBlock.size()); + } + + long centralDirOffset = ZipUtils.getZipEocdCentralDirectoryOffset(eocd); + long signingBlockSize = centralDirOffset - beforeApkSigningBlock.size(); + if (signingBlockSize % ANDROID_COMMON_PAGE_ALIGNMENT_BYTES != 0) { + throw new RuntimeException( + "APK Signing Block size is not multiple of page size: " + + signingBlockSize); + } + } + } catch (DigestException e) { + throw new RuntimeException("Failed to compute content digests", e); + } + if (!contentDigestAlgorithms.equals(actualContentDigests.keySet())) { + throw new RuntimeException( + "Mismatch between sets of requested and computed content digests" + + " . Requested: " + contentDigestAlgorithms + + ", computed: " + actualContentDigests.keySet()); + } + + // Compare digests computed over the rest of APK against the corresponding expected digests + // in signer blocks. + for (Result.SignerInfo signerInfo : result.signers) { + for (Result.SignerInfo.ContentDigest expected : signerInfo.contentDigests) { + SignatureAlgorithm signatureAlgorithm = + SignatureAlgorithm.findById(expected.getSignatureAlgorithmId()); + if (signatureAlgorithm == null) { + continue; + } + ContentDigestAlgorithm contentDigestAlgorithm = + signatureAlgorithm.getContentDigestAlgorithm(); + // if the current digest algorithm is not in the list provided by the caller then + // ignore it; the signer may contain digests not recognized by the specified SDK + // range. + if (!contentDigestAlgorithms.contains(contentDigestAlgorithm)) { + continue; + } + byte[] expectedDigest = expected.getValue(); + byte[] actualDigest = actualContentDigests.get(contentDigestAlgorithm); + if (!Arrays.equals(expectedDigest, actualDigest)) { + if (result.signatureSchemeVersion == VERSION_APK_SIGNATURE_SCHEME_V2) { + signerInfo.addError( + ApkVerifier.Issue.V2_SIG_APK_DIGEST_DID_NOT_VERIFY, + contentDigestAlgorithm, + toHex(expectedDigest), + toHex(actualDigest)); + } else if (result.signatureSchemeVersion == VERSION_APK_SIGNATURE_SCHEME_V3) { + signerInfo.addError( + ApkVerifier.Issue.V3_SIG_APK_DIGEST_DID_NOT_VERIFY, + contentDigestAlgorithm, + toHex(expectedDigest), + toHex(actualDigest)); + } + continue; + } + signerInfo.verifiedContentDigests.put(contentDigestAlgorithm, actualDigest); + } + } + } + + public static ByteBuffer findApkSignatureSchemeBlock( + ByteBuffer apkSigningBlock, + int blockId, + Result result) throws SignatureNotFoundException { + try { + return ApkSigningBlockUtilsLite.findApkSignatureSchemeBlock(apkSigningBlock, blockId); + } catch (com.android.apksig.internal.apk.SignatureNotFoundException e) { + throw new SignatureNotFoundException(e.getMessage()); + } + } + + public static void checkByteOrderLittleEndian(ByteBuffer buffer) { + ApkSigningBlockUtilsLite.checkByteOrderLittleEndian(buffer); + } + + public static ByteBuffer getLengthPrefixedSlice(ByteBuffer source) throws ApkFormatException { + return ApkSigningBlockUtilsLite.getLengthPrefixedSlice(source); + } + + public static byte[] readLengthPrefixedByteArray(ByteBuffer buf) throws ApkFormatException { + return ApkSigningBlockUtilsLite.readLengthPrefixedByteArray(buf); + } + + public static String toHex(byte[] value) { + return ApkSigningBlockUtilsLite.toHex(value); + } + + public static Map<ContentDigestAlgorithm, byte[]> computeContentDigests( + RunnablesExecutor executor, + Set<ContentDigestAlgorithm> digestAlgorithms, + DataSource beforeCentralDir, + DataSource centralDir, + DataSource eocd) throws IOException, NoSuchAlgorithmException, DigestException { + Map<ContentDigestAlgorithm, byte[]> contentDigests = new HashMap<>(); + Set<ContentDigestAlgorithm> oneMbChunkBasedAlgorithm = new HashSet<>(); + for (ContentDigestAlgorithm digestAlgorithm : digestAlgorithms) { + if (digestAlgorithm == ContentDigestAlgorithm.CHUNKED_SHA256 + || digestAlgorithm == ContentDigestAlgorithm.CHUNKED_SHA512) { + oneMbChunkBasedAlgorithm.add(digestAlgorithm); + } + } + computeOneMbChunkContentDigests( + executor, + oneMbChunkBasedAlgorithm, + new DataSource[] { beforeCentralDir, centralDir, eocd }, + contentDigests); + + if (digestAlgorithms.contains(VERITY_CHUNKED_SHA256)) { + computeApkVerityDigest(beforeCentralDir, centralDir, eocd, contentDigests); + } + return contentDigests; + } + + static void computeOneMbChunkContentDigests( + Set<ContentDigestAlgorithm> digestAlgorithms, + DataSource[] contents, + Map<ContentDigestAlgorithm, byte[]> outputContentDigests) + throws IOException, NoSuchAlgorithmException, DigestException { + // For each digest algorithm the result is computed as follows: + // 1. Each segment of contents is split into consecutive chunks of 1 MB in size. + // The final chunk will be shorter iff the length of segment is not a multiple of 1 MB. + // No chunks are produced for empty (zero length) segments. + // 2. The digest of each chunk is computed over the concatenation of byte 0xa5, the chunk's + // length in bytes (uint32 little-endian) and the chunk's contents. + // 3. The output digest is computed over the concatenation of the byte 0x5a, the number of + // chunks (uint32 little-endian) and the concatenation of digests of chunks of all + // segments in-order. + + long chunkCountLong = 0; + for (DataSource input : contents) { + chunkCountLong += + getChunkCount(input.size(), CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES); + } + if (chunkCountLong > Integer.MAX_VALUE) { + throw new DigestException("Input too long: " + chunkCountLong + " chunks"); + } + int chunkCount = (int) chunkCountLong; + + ContentDigestAlgorithm[] digestAlgorithmsArray = + digestAlgorithms.toArray(new ContentDigestAlgorithm[digestAlgorithms.size()]); + MessageDigest[] mds = new MessageDigest[digestAlgorithmsArray.length]; + byte[][] digestsOfChunks = new byte[digestAlgorithmsArray.length][]; + int[] digestOutputSizes = new int[digestAlgorithmsArray.length]; + for (int i = 0; i < digestAlgorithmsArray.length; i++) { + ContentDigestAlgorithm digestAlgorithm = digestAlgorithmsArray[i]; + int digestOutputSizeBytes = digestAlgorithm.getChunkDigestOutputSizeBytes(); + digestOutputSizes[i] = digestOutputSizeBytes; + byte[] concatenationOfChunkCountAndChunkDigests = + new byte[5 + chunkCount * digestOutputSizeBytes]; + concatenationOfChunkCountAndChunkDigests[0] = 0x5a; + setUnsignedInt32LittleEndian( + chunkCount, concatenationOfChunkCountAndChunkDigests, 1); + digestsOfChunks[i] = concatenationOfChunkCountAndChunkDigests; + String jcaAlgorithm = digestAlgorithm.getJcaMessageDigestAlgorithm(); + mds[i] = MessageDigest.getInstance(jcaAlgorithm); + } + + DataSink mdSink = DataSinks.asDataSink(mds); + byte[] chunkContentPrefix = new byte[5]; + chunkContentPrefix[0] = (byte) 0xa5; + int chunkIndex = 0; + // Optimization opportunity: digests of chunks can be computed in parallel. However, + // determining the number of computations to be performed in parallel is non-trivial. This + // depends on a wide range of factors, such as data source type (e.g., in-memory or fetched + // from file), CPU/memory/disk cache bandwidth and latency, interconnect architecture of CPU + // cores, load on the system from other threads of execution and other processes, size of + // input. + // For now, we compute these digests sequentially and thus have the luxury of improving + // performance by writing the digest of each chunk into a pre-allocated buffer at exactly + // the right position. This avoids unnecessary allocations, copying, and enables the final + // digest to be more efficient because it's presented with all of its input in one go. + for (DataSource input : contents) { + long inputOffset = 0; + long inputRemaining = input.size(); + while (inputRemaining > 0) { + int chunkSize = + (int) Math.min(inputRemaining, CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES); + setUnsignedInt32LittleEndian(chunkSize, chunkContentPrefix, 1); + for (int i = 0; i < mds.length; i++) { + mds[i].update(chunkContentPrefix); + } + try { + input.feed(inputOffset, chunkSize, mdSink); + } catch (IOException e) { + throw new IOException("Failed to read chunk #" + chunkIndex, e); + } + for (int i = 0; i < digestAlgorithmsArray.length; i++) { + MessageDigest md = mds[i]; + byte[] concatenationOfChunkCountAndChunkDigests = digestsOfChunks[i]; + int expectedDigestSizeBytes = digestOutputSizes[i]; + int actualDigestSizeBytes = + md.digest( + concatenationOfChunkCountAndChunkDigests, + 5 + chunkIndex * expectedDigestSizeBytes, + expectedDigestSizeBytes); + if (actualDigestSizeBytes != expectedDigestSizeBytes) { + throw new RuntimeException( + "Unexpected output size of " + md.getAlgorithm() + + " digest: " + actualDigestSizeBytes); + } + } + inputOffset += chunkSize; + inputRemaining -= chunkSize; + chunkIndex++; + } + } + + for (int i = 0; i < digestAlgorithmsArray.length; i++) { + ContentDigestAlgorithm digestAlgorithm = digestAlgorithmsArray[i]; + byte[] concatenationOfChunkCountAndChunkDigests = digestsOfChunks[i]; + MessageDigest md = mds[i]; + byte[] digest = md.digest(concatenationOfChunkCountAndChunkDigests); + outputContentDigests.put(digestAlgorithm, digest); + } + } + + static void computeOneMbChunkContentDigests( + RunnablesExecutor executor, + Set<ContentDigestAlgorithm> digestAlgorithms, + DataSource[] contents, + Map<ContentDigestAlgorithm, byte[]> outputContentDigests) + throws NoSuchAlgorithmException, DigestException { + long chunkCountLong = 0; + for (DataSource input : contents) { + chunkCountLong += + getChunkCount(input.size(), CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES); + } + if (chunkCountLong > Integer.MAX_VALUE) { + throw new DigestException("Input too long: " + chunkCountLong + " chunks"); + } + int chunkCount = (int) chunkCountLong; + + List<ChunkDigests> chunkDigestsList = new ArrayList<>(digestAlgorithms.size()); + for (ContentDigestAlgorithm algorithms : digestAlgorithms) { + chunkDigestsList.add(new ChunkDigests(algorithms, chunkCount)); + } + + ChunkSupplier chunkSupplier = new ChunkSupplier(contents); + executor.execute(() -> new ChunkDigester(chunkSupplier, chunkDigestsList)); + + // Compute and write out final digest for each algorithm. + for (ChunkDigests chunkDigests : chunkDigestsList) { + MessageDigest messageDigest = chunkDigests.createMessageDigest(); + outputContentDigests.put( + chunkDigests.algorithm, + messageDigest.digest(chunkDigests.concatOfDigestsOfChunks)); + } + } + + private static class ChunkDigests { + private final ContentDigestAlgorithm algorithm; + private final int digestOutputSize; + private final byte[] concatOfDigestsOfChunks; + + private ChunkDigests(ContentDigestAlgorithm algorithm, int chunkCount) { + this.algorithm = algorithm; + digestOutputSize = this.algorithm.getChunkDigestOutputSizeBytes(); + concatOfDigestsOfChunks = new byte[1 + 4 + chunkCount * digestOutputSize]; + + // Fill the initial values of the concatenated digests of chunks, which is + // {0x5a, 4-bytes-of-little-endian-chunk-count, digests*...}. + concatOfDigestsOfChunks[0] = 0x5a; + setUnsignedInt32LittleEndian(chunkCount, concatOfDigestsOfChunks, 1); + } + + private MessageDigest createMessageDigest() throws NoSuchAlgorithmException { + return MessageDigest.getInstance(algorithm.getJcaMessageDigestAlgorithm()); + } + + private int getOffset(int chunkIndex) { + return 1 + 4 + chunkIndex * digestOutputSize; + } + } + + /** + * A per-thread digest worker. + */ + private static class ChunkDigester implements Runnable { + private final ChunkSupplier dataSupplier; + private final List<ChunkDigests> chunkDigests; + private final List<MessageDigest> messageDigests; + private final DataSink mdSink; + + private ChunkDigester(ChunkSupplier dataSupplier, List<ChunkDigests> chunkDigests) { + this.dataSupplier = dataSupplier; + this.chunkDigests = chunkDigests; + messageDigests = new ArrayList<>(chunkDigests.size()); + for (ChunkDigests chunkDigest : chunkDigests) { + try { + messageDigests.add(chunkDigest.createMessageDigest()); + } catch (NoSuchAlgorithmException ex) { + throw new RuntimeException(ex); + } + } + mdSink = DataSinks.asDataSink(messageDigests.toArray(new MessageDigest[0])); + } + + @Override + public void run() { + byte[] chunkContentPrefix = new byte[5]; + chunkContentPrefix[0] = (byte) 0xa5; + + try { + for (ChunkSupplier.Chunk chunk = dataSupplier.get(); + chunk != null; + chunk = dataSupplier.get()) { + int size = chunk.size; + if (size > CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES) { + throw new RuntimeException("Chunk size greater than expected: " + size); + } + + // First update with the chunk prefix. + setUnsignedInt32LittleEndian(size, chunkContentPrefix, 1); + mdSink.consume(chunkContentPrefix, 0, chunkContentPrefix.length); + + // Then update with the chunk data. + mdSink.consume(chunk.data); + + // Now finalize chunk for all algorithms. + for (int i = 0; i < chunkDigests.size(); i++) { + ChunkDigests chunkDigest = chunkDigests.get(i); + int actualDigestSize = messageDigests.get(i).digest( + chunkDigest.concatOfDigestsOfChunks, + chunkDigest.getOffset(chunk.chunkIndex), + chunkDigest.digestOutputSize); + if (actualDigestSize != chunkDigest.digestOutputSize) { + throw new RuntimeException( + "Unexpected output size of " + chunkDigest.algorithm + + " digest: " + actualDigestSize); + } + } + } + } catch (IOException | DigestException e) { + throw new RuntimeException(e); + } + } + } + + /** + * Thread-safe 1MB DataSource chunk supplier. When bounds are met in a + * supplied {@link DataSource}, the data from the next {@link DataSource} + * are NOT concatenated. Only the next call to get() will fetch from the + * next {@link DataSource} in the input {@link DataSource} array. + */ + private static class ChunkSupplier implements Supplier<ChunkSupplier.Chunk> { + private final DataSource[] dataSources; + private final int[] chunkCounts; + private final int totalChunkCount; + private final AtomicInteger nextIndex; + + private ChunkSupplier(DataSource[] dataSources) { + this.dataSources = dataSources; + chunkCounts = new int[dataSources.length]; + int totalChunkCount = 0; + for (int i = 0; i < dataSources.length; i++) { + long chunkCount = getChunkCount(dataSources[i].size(), + CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES); + if (chunkCount > Integer.MAX_VALUE) { + throw new RuntimeException( + String.format( + "Number of chunks in dataSource[%d] is greater than max int.", + i)); + } + chunkCounts[i] = (int)chunkCount; + totalChunkCount = (int) (totalChunkCount + chunkCount); + } + this.totalChunkCount = totalChunkCount; + nextIndex = new AtomicInteger(0); + } + + /** + * We map an integer index to the termination-adjusted dataSources 1MB chunks. + * Note that {@link Chunk}s could be less than 1MB, namely the last 1MB-aligned + * blocks in each input {@link DataSource} (unless the DataSource itself is + * 1MB-aligned). + */ + @Override + public ChunkSupplier.Chunk get() { + int index = nextIndex.getAndIncrement(); + if (index < 0 || index >= totalChunkCount) { + return null; + } + + int dataSourceIndex = 0; + long dataSourceChunkOffset = index; + for (; dataSourceIndex < dataSources.length; dataSourceIndex++) { + if (dataSourceChunkOffset < chunkCounts[dataSourceIndex]) { + break; + } + dataSourceChunkOffset -= chunkCounts[dataSourceIndex]; + } + + long remainingSize = Math.min( + dataSources[dataSourceIndex].size() - + dataSourceChunkOffset * CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES, + CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES); + + final int size = (int)remainingSize; + final ByteBuffer buffer = ByteBuffer.allocate(size); + try { + dataSources[dataSourceIndex].copyTo( + dataSourceChunkOffset * CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES, size, + buffer); + } catch (IOException e) { + throw new IllegalStateException("Failed to read chunk", e); + } + buffer.rewind(); + + return new Chunk(index, buffer, size); + } + + static class Chunk { + private final int chunkIndex; + private final ByteBuffer data; + private final int size; + + private Chunk(int chunkIndex, ByteBuffer data, int size) { + this.chunkIndex = chunkIndex; + this.data = data; + this.size = size; + } + } + } + + @SuppressWarnings("ByteBufferBackingArray") + private static void computeApkVerityDigest(DataSource beforeCentralDir, DataSource centralDir, + DataSource eocd, Map<ContentDigestAlgorithm, byte[]> outputContentDigests) + throws IOException, NoSuchAlgorithmException { + ByteBuffer encoded = createVerityDigestBuffer(true); + // Use 0s as salt for now. This also needs to be consistent in the fsverify header for + // kernel to use. + try (VerityTreeBuilder builder = new VerityTreeBuilder(new byte[8])) { + byte[] rootHash = builder.generateVerityTreeRootHash(beforeCentralDir, centralDir, + eocd); + encoded.put(rootHash); + encoded.putLong(beforeCentralDir.size() + centralDir.size() + eocd.size()); + outputContentDigests.put(VERITY_CHUNKED_SHA256, encoded.array()); + } + } + + private static ByteBuffer createVerityDigestBuffer(boolean includeSourceDataSize) { + // FORMAT: + // OFFSET DATA TYPE DESCRIPTION + // * @+0 bytes uint8[32] Merkle tree root hash of SHA-256 + // * @+32 bytes int64 (optional) Length of source data + int backBufferSize = + VERITY_CHUNKED_SHA256.getChunkDigestOutputSizeBytes(); + if (includeSourceDataSize) { + backBufferSize += Long.SIZE / Byte.SIZE; + } + ByteBuffer encoded = ByteBuffer.allocate(backBufferSize); + encoded.order(ByteOrder.LITTLE_ENDIAN); + return encoded; + } + + public static class VerityTreeAndDigest { + public final ContentDigestAlgorithm contentDigestAlgorithm; + public final byte[] rootHash; + public final byte[] tree; + + VerityTreeAndDigest(ContentDigestAlgorithm contentDigestAlgorithm, byte[] rootHash, + byte[] tree) { + this.contentDigestAlgorithm = contentDigestAlgorithm; + this.rootHash = rootHash; + this.tree = tree; + } + } + + @SuppressWarnings("ByteBufferBackingArray") + public static VerityTreeAndDigest computeChunkVerityTreeAndDigest(DataSource dataSource) + throws IOException, NoSuchAlgorithmException { + ByteBuffer encoded = createVerityDigestBuffer(false); + // Use 0s as salt for now. This also needs to be consistent in the fsverify header for + // kernel to use. + try (VerityTreeBuilder builder = new VerityTreeBuilder(null)) { + ByteBuffer tree = builder.generateVerityTree(dataSource); + byte[] rootHash = builder.getRootHashFromTree(tree); + encoded.put(rootHash); + return new VerityTreeAndDigest(VERITY_CHUNKED_SHA256, encoded.array(), tree.array()); + } + } + + private static long getChunkCount(long inputSize, long chunkSize) { + return (inputSize + chunkSize - 1) / chunkSize; + } + + private static void setUnsignedInt32LittleEndian(int value, byte[] result, int offset) { + result[offset] = (byte) (value & 0xff); + result[offset + 1] = (byte) ((value >> 8) & 0xff); + result[offset + 2] = (byte) ((value >> 16) & 0xff); + result[offset + 3] = (byte) ((value >> 24) & 0xff); + } + + public static byte[] encodePublicKey(PublicKey publicKey) + throws InvalidKeyException, NoSuchAlgorithmException { + byte[] encodedPublicKey = null; + if ("X.509".equals(publicKey.getFormat())) { + encodedPublicKey = publicKey.getEncoded(); + // if the key is an RSA key check for a negative modulus + String keyAlgorithm = publicKey.getAlgorithm(); + if ("RSA".equals(keyAlgorithm) || OID_RSA_ENCRYPTION.equals(keyAlgorithm)) { + try { + // Parse the encoded public key into the separate elements of the + // SubjectPublicKeyInfo to obtain the SubjectPublicKey. + ByteBuffer encodedPublicKeyBuffer = ByteBuffer.wrap(encodedPublicKey); + SubjectPublicKeyInfo subjectPublicKeyInfo = Asn1BerParser.parse( + encodedPublicKeyBuffer, SubjectPublicKeyInfo.class); + // The SubjectPublicKey is encoded as a bit string within the + // SubjectPublicKeyInfo. The first byte of the encoding is the number of padding + // bits; store this and decode the rest of the bit string into the RSA modulus + // and exponent. + ByteBuffer subjectPublicKeyBuffer = subjectPublicKeyInfo.subjectPublicKey; + byte padding = subjectPublicKeyBuffer.get(); + RSAPublicKey rsaPublicKey = Asn1BerParser.parse(subjectPublicKeyBuffer, + RSAPublicKey.class); + // if the modulus is negative then attempt to reencode it with a leading 0 sign + // byte. + if (rsaPublicKey.modulus.compareTo(BigInteger.ZERO) < 0) { + // A negative modulus indicates the leading bit in the integer is 1. Per + // ASN.1 encoding rules to encode a positive integer with the leading bit + // set to 1 a byte containing all zeros should precede the integer encoding. + byte[] encodedModulus = rsaPublicKey.modulus.toByteArray(); + byte[] reencodedModulus = new byte[encodedModulus.length + 1]; + reencodedModulus[0] = 0; + System.arraycopy(encodedModulus, 0, reencodedModulus, 1, + encodedModulus.length); + rsaPublicKey.modulus = new BigInteger(reencodedModulus); + // Once the modulus has been corrected reencode the RSAPublicKey, then + // restore the padding value in the bit string and reencode the entire + // SubjectPublicKeyInfo to be returned to the caller. + byte[] reencodedRSAPublicKey = Asn1DerEncoder.encode(rsaPublicKey); + byte[] reencodedSubjectPublicKey = + new byte[reencodedRSAPublicKey.length + 1]; + reencodedSubjectPublicKey[0] = padding; + System.arraycopy(reencodedRSAPublicKey, 0, reencodedSubjectPublicKey, 1, + reencodedRSAPublicKey.length); + subjectPublicKeyInfo.subjectPublicKey = ByteBuffer.wrap( + reencodedSubjectPublicKey); + encodedPublicKey = Asn1DerEncoder.encode(subjectPublicKeyInfo); + } + } catch (Asn1DecodingException | Asn1EncodingException e) { + System.out.println("Caught a exception encoding the public key: " + e); + e.printStackTrace(); + encodedPublicKey = null; + } + } + } + if (encodedPublicKey == null) { + try { + encodedPublicKey = + KeyFactory.getInstance(publicKey.getAlgorithm()) + .getKeySpec(publicKey, X509EncodedKeySpec.class) + .getEncoded(); + } catch (InvalidKeySpecException e) { + throw new InvalidKeyException( + "Failed to obtain X.509 encoded form of public key " + publicKey + + " of class " + publicKey.getClass().getName(), + e); + } + } + if ((encodedPublicKey == null) || (encodedPublicKey.length == 0)) { + throw new InvalidKeyException( + "Failed to obtain X.509 encoded form of public key " + publicKey + + " of class " + publicKey.getClass().getName()); + } + return encodedPublicKey; + } + + public static List<byte[]> encodeCertificates(List<X509Certificate> certificates) + throws CertificateEncodingException { + List<byte[]> result = new ArrayList<>(certificates.size()); + for (X509Certificate certificate : certificates) { + result.add(certificate.getEncoded()); + } + return result; + } + + public static byte[] encodeAsLengthPrefixedElement(byte[] bytes) { + byte[][] adapterBytes = new byte[1][]; + adapterBytes[0] = bytes; + return encodeAsSequenceOfLengthPrefixedElements(adapterBytes); + } + + public static byte[] encodeAsSequenceOfLengthPrefixedElements(List<byte[]> sequence) { + return encodeAsSequenceOfLengthPrefixedElements( + sequence.toArray(new byte[sequence.size()][])); + } + + public static byte[] encodeAsSequenceOfLengthPrefixedElements(byte[][] sequence) { + int payloadSize = 0; + for (byte[] element : sequence) { + payloadSize += 4 + element.length; + } + ByteBuffer result = ByteBuffer.allocate(payloadSize); + result.order(ByteOrder.LITTLE_ENDIAN); + for (byte[] element : sequence) { + result.putInt(element.length); + result.put(element); + } + return result.array(); + } + + public static byte[] encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes( + List<Pair<Integer, byte[]>> sequence) { + return ApkSigningBlockUtilsLite + .encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(sequence); + } + + /** + * Returns the APK Signature Scheme block contained in the provided APK file for the given ID + * and the additional information relevant for verifying the block against the file. + * + * @param blockId the ID value in the APK Signing Block's sequence of ID-value pairs + * identifying the appropriate block to find, e.g. the APK Signature Scheme v2 + * block ID. + * + * @throws SignatureNotFoundException if the APK is not signed using given APK Signature Scheme + * @throws IOException if an I/O error occurs while reading the APK + */ + public static SignatureInfo findSignature( + DataSource apk, ApkUtils.ZipSections zipSections, int blockId, Result result) + throws IOException, SignatureNotFoundException { + try { + return ApkSigningBlockUtilsLite.findSignature(apk, zipSections, blockId); + } catch (com.android.apksig.internal.apk.SignatureNotFoundException e) { + throw new SignatureNotFoundException(e.getMessage()); + } + } + + /** + * Generates a new DataSource representing the APK contents before the Central Directory with + * padding, if padding is requested. If the existing data entries before the Central Directory + * are already aligned, or no padding is requested, the original DataSource is used. This + * padding is used to allow for verity-based APK verification. + * + * @return {@code Pair} containing the potentially new {@code DataSource} and the amount of + * padding used. + */ + public static Pair<DataSource, Integer> generateApkSigningBlockPadding( + DataSource beforeCentralDir, + boolean apkSigningBlockPaddingSupported) { + + // Ensure APK Signing Block starts from page boundary. + int padSizeBeforeSigningBlock = 0; + if (apkSigningBlockPaddingSupported && + (beforeCentralDir.size() % ANDROID_COMMON_PAGE_ALIGNMENT_BYTES != 0)) { + padSizeBeforeSigningBlock = (int) ( + ANDROID_COMMON_PAGE_ALIGNMENT_BYTES - + beforeCentralDir.size() % ANDROID_COMMON_PAGE_ALIGNMENT_BYTES); + beforeCentralDir = new ChainedDataSource( + beforeCentralDir, + DataSources.asDataSource( + ByteBuffer.allocate(padSizeBeforeSigningBlock))); + } + return Pair.of(beforeCentralDir, padSizeBeforeSigningBlock); + } + + public static DataSource copyWithModifiedCDOffset( + DataSource beforeCentralDir, DataSource eocd) throws IOException { + + // Ensure that, when digesting, ZIP End of Central Directory record's Central Directory + // offset field is treated as pointing to the offset at which the APK Signing Block will + // start. + long centralDirOffsetForDigesting = beforeCentralDir.size(); + ByteBuffer eocdBuf = ByteBuffer.allocate((int) eocd.size()); + eocdBuf.order(ByteOrder.LITTLE_ENDIAN); + eocd.copyTo(0, (int) eocd.size(), eocdBuf); + eocdBuf.flip(); + ZipUtils.setZipEocdCentralDirectoryOffset(eocdBuf, centralDirOffsetForDigesting); + return DataSources.asDataSource(eocdBuf); + } + + public static byte[] generateApkSigningBlock( + List<Pair<byte[], Integer>> apkSignatureSchemeBlockPairs) { + // FORMAT: + // uint64: size (excluding this field) + // repeated ID-value pairs: + // uint64: size (excluding this field) + // uint32: ID + // (size - 4) bytes: value + // (extra verity ID-value for padding to make block size a multiple of 4096 bytes) + // uint64: size (same as the one above) + // uint128: magic + + int blocksSize = 0; + for (Pair<byte[], Integer> schemeBlockPair : apkSignatureSchemeBlockPairs) { + blocksSize += 8 + 4 + schemeBlockPair.getFirst().length; // size + id + value + } + + int resultSize = + 8 // size + + blocksSize + + 8 // size + + 16 // magic + ; + ByteBuffer paddingPair = null; + if (resultSize % ANDROID_COMMON_PAGE_ALIGNMENT_BYTES != 0) { + int padding = ANDROID_COMMON_PAGE_ALIGNMENT_BYTES - + (resultSize % ANDROID_COMMON_PAGE_ALIGNMENT_BYTES); + if (padding < 12) { // minimum size of an ID-value pair + padding += ANDROID_COMMON_PAGE_ALIGNMENT_BYTES; + } + paddingPair = ByteBuffer.allocate(padding).order(ByteOrder.LITTLE_ENDIAN); + paddingPair.putLong(padding - 8); + paddingPair.putInt(VERITY_PADDING_BLOCK_ID); + paddingPair.rewind(); + resultSize += padding; + } + + ByteBuffer result = ByteBuffer.allocate(resultSize); + result.order(ByteOrder.LITTLE_ENDIAN); + long blockSizeFieldValue = resultSize - 8L; + result.putLong(blockSizeFieldValue); + + for (Pair<byte[], Integer> schemeBlockPair : apkSignatureSchemeBlockPairs) { + byte[] apkSignatureSchemeBlock = schemeBlockPair.getFirst(); + int apkSignatureSchemeId = schemeBlockPair.getSecond(); + long pairSizeFieldValue = 4L + apkSignatureSchemeBlock.length; + result.putLong(pairSizeFieldValue); + result.putInt(apkSignatureSchemeId); + result.put(apkSignatureSchemeBlock); + } + + if (paddingPair != null) { + result.put(paddingPair); + } + + result.putLong(blockSizeFieldValue); + result.put(APK_SIGNING_BLOCK_MAGIC); + + return result.array(); + } + + /** + * Returns the individual APK signature blocks within the provided {@code apkSigningBlock} in a + * {@code List} of {@code Pair} instances where the first element in the {@code Pair} is the + * contents / value of the signature block and the second element is the ID of the block. + * + * @throws IOException if an error is encountered reading the provided {@code apkSigningBlock} + */ + public static List<Pair<byte[], Integer>> getApkSignatureBlocks( + DataSource apkSigningBlock) throws IOException { + // FORMAT: + // uint64: size (excluding this field) + // repeated ID-value pairs: + // uint64: size (excluding this field) + // uint32: ID + // (size - 4) bytes: value + // (extra verity ID-value for padding to make block size a multiple of 4096 bytes) + // uint64: size (same as the one above) + // uint128: magic + long apkSigningBlockSize = apkSigningBlock.size(); + if (apkSigningBlock.size() > Integer.MAX_VALUE || apkSigningBlockSize < 32) { + throw new IllegalArgumentException( + "APK signing block size out of range: " + apkSigningBlockSize); + } + // Remove the header and footer from the signing block to iterate over only the repeated + // ID-value pairs. + ByteBuffer apkSigningBlockBuffer = apkSigningBlock.getByteBuffer(8, + (int) apkSigningBlock.size() - 32); + apkSigningBlockBuffer.order(ByteOrder.LITTLE_ENDIAN); + List<Pair<byte[], Integer>> signatureBlocks = new ArrayList<>(); + while (apkSigningBlockBuffer.hasRemaining()) { + long blockLength = apkSigningBlockBuffer.getLong(); + if (blockLength > Integer.MAX_VALUE || blockLength < 4) { + throw new IllegalArgumentException( + "Block index " + (signatureBlocks.size() + 1) + " size out of range: " + + blockLength); + } + int blockId = apkSigningBlockBuffer.getInt(); + // Since the block ID has already been read from the signature block read the next + // blockLength - 4 bytes as the value. + byte[] blockValue = new byte[(int) blockLength - 4]; + apkSigningBlockBuffer.get(blockValue); + signatureBlocks.add(Pair.of(blockValue, blockId)); + } + return signatureBlocks; + } + + /** + * Returns the individual APK signers within the provided {@code signatureBlock} in a {@code + * List} of {@code Pair} instances where the first element is a {@code List} of {@link + * X509Certificate}s and the second element is a byte array of the individual signer's block. + * + * <p>This method supports any signature block that adheres to the following format up to the + * signing certificate(s): + * <pre> + * * length-prefixed sequence of length-prefixed signers + * * length-prefixed signed data + * * length-prefixed sequence of length-prefixed digests: + * * uint32: signature algorithm ID + * * length-prefixed bytes: digest of contents + * * length-prefixed sequence of certificates: + * * length-prefixed bytes: X.509 certificate (ASN.1 DER encoded). + * </pre> + * + * <p>Note, this is a convenience method to obtain any signers from an existing signature block; + * the signature of each signer will not be verified. + * + * @throws ApkFormatException if an error is encountered while parsing the provided {@code + * signatureBlock} + * @throws CertificateException if the signing certificate(s) within an individual signer block + * cannot be parsed + */ + public static List<Pair<List<X509Certificate>, byte[]>> getApkSignatureBlockSigners( + byte[] signatureBlock) throws ApkFormatException, CertificateException { + ByteBuffer signatureBlockBuffer = ByteBuffer.wrap(signatureBlock); + signatureBlockBuffer.order(ByteOrder.LITTLE_ENDIAN); + ByteBuffer signersBuffer = getLengthPrefixedSlice(signatureBlockBuffer); + List<Pair<List<X509Certificate>, byte[]>> signers = new ArrayList<>(); + while (signersBuffer.hasRemaining()) { + // Parse the next signer block, save all of its bytes for the resulting List, and + // rewind the buffer to allow the signing certificate(s) to be parsed. + ByteBuffer signer = getLengthPrefixedSlice(signersBuffer); + byte[] signerBytes = new byte[signer.remaining()]; + signer.get(signerBytes); + signer.rewind(); + + ByteBuffer signedData = getLengthPrefixedSlice(signer); + // The first length prefixed slice is the sequence of digests which are not required + // when obtaining the signing certificate(s). + getLengthPrefixedSlice(signedData); + ByteBuffer certificatesBuffer = getLengthPrefixedSlice(signedData); + List<X509Certificate> certificates = new ArrayList<>(); + while (certificatesBuffer.hasRemaining()) { + int certLength = certificatesBuffer.getInt(); + byte[] certBytes = new byte[certLength]; + if (certLength > certificatesBuffer.remaining()) { + throw new IllegalArgumentException( + "Cert index " + (certificates.size() + 1) + " under signer index " + + (signers.size() + 1) + " size out of range: " + certLength); + } + certificatesBuffer.get(certBytes); + GuaranteedEncodedFormX509Certificate signerCert = + new GuaranteedEncodedFormX509Certificate( + X509CertificateUtils.generateCertificate(certBytes), certBytes); + certificates.add(signerCert); + } + signers.add(Pair.of(certificates, signerBytes)); + } + return signers; + } + + /** + * Computes the digests of the given APK components according to the algorithms specified in the + * given SignerConfigs. + * + * @param signerConfigs signer configurations, one for each signer At least one signer config + * must be provided. + * + * @throws IOException if an I/O error occurs + * @throws NoSuchAlgorithmException if a required cryptographic algorithm implementation is + * missing + * @throws SignatureException if an error occurs when computing digests of generating + * signatures + */ + public static Pair<List<SignerConfig>, Map<ContentDigestAlgorithm, byte[]>> + computeContentDigests( + RunnablesExecutor executor, + DataSource beforeCentralDir, + DataSource centralDir, + DataSource eocd, + List<SignerConfig> signerConfigs) + throws IOException, NoSuchAlgorithmException, SignatureException { + if (signerConfigs.isEmpty()) { + throw new IllegalArgumentException( + "No signer configs provided. At least one is required"); + } + + // Figure out which digest(s) to use for APK contents. + Set<ContentDigestAlgorithm> contentDigestAlgorithms = new HashSet<>(1); + for (SignerConfig signerConfig : signerConfigs) { + for (SignatureAlgorithm signatureAlgorithm : signerConfig.signatureAlgorithms) { + contentDigestAlgorithms.add(signatureAlgorithm.getContentDigestAlgorithm()); + } + } + + // Compute digests of APK contents. + Map<ContentDigestAlgorithm, byte[]> contentDigests; // digest algorithm ID -> digest + try { + contentDigests = + computeContentDigests( + executor, + contentDigestAlgorithms, + beforeCentralDir, + centralDir, + eocd); + } catch (IOException e) { + throw new IOException("Failed to read APK being signed", e); + } catch (DigestException e) { + throw new SignatureException("Failed to compute digests of APK", e); + } + + // Sign the digests and wrap the signatures and signer info into an APK Signing Block. + return Pair.of(signerConfigs, contentDigests); + } + + /** + * Returns the subset of signatures which are expected to be verified by at least one Android + * platform version in the {@code [minSdkVersion, maxSdkVersion]} range. The returned result is + * guaranteed to contain at least one signature. + * + * <p>Each Android platform version typically verifies exactly one signature from the provided + * {@code signatures} set. This method returns the set of these signatures collected over all + * requested platform versions. As a result, the result may contain more than one signature. + * + * @throws NoSupportedSignaturesException if no supported signatures were + * found for an Android platform version in the range. + */ + public static <T extends ApkSupportedSignature> List<T> getSignaturesToVerify( + List<T> signatures, int minSdkVersion, int maxSdkVersion) + throws NoSupportedSignaturesException { + return getSignaturesToVerify(signatures, minSdkVersion, maxSdkVersion, false); + } + + /** + * Returns the subset of signatures which are expected to be verified by at least one Android + * platform version in the {@code [minSdkVersion, maxSdkVersion]} range. The returned result is + * guaranteed to contain at least one signature. + * + * <p>{@code onlyRequireJcaSupport} can be set to true for cases that only require verifying a + * signature within the signing block using the standard JCA. + * + * <p>Each Android platform version typically verifies exactly one signature from the provided + * {@code signatures} set. This method returns the set of these signatures collected over all + * requested platform versions. As a result, the result may contain more than one signature. + * + * @throws NoSupportedSignaturesException if no supported signatures were + * found for an Android platform version in the range. + */ + public static <T extends ApkSupportedSignature> List<T> getSignaturesToVerify( + List<T> signatures, int minSdkVersion, int maxSdkVersion, + boolean onlyRequireJcaSupport) throws NoSupportedSignaturesException { + try { + return ApkSigningBlockUtilsLite.getSignaturesToVerify(signatures, minSdkVersion, + maxSdkVersion, onlyRequireJcaSupport); + } catch (NoApkSupportedSignaturesException e) { + throw new NoSupportedSignaturesException(e.getMessage()); + } + } + + public static class NoSupportedSignaturesException extends NoApkSupportedSignaturesException { + public NoSupportedSignaturesException(String message) { + super(message); + } + } + + public static class SignatureNotFoundException extends Exception { + private static final long serialVersionUID = 1L; + + public SignatureNotFoundException(String message) { + super(message); + } + + public SignatureNotFoundException(String message, Throwable cause) { + super(message, cause); + } + } + + /** + * uses the SignatureAlgorithms in the provided signerConfig to sign the provided data + * + * @return list of signature algorithm IDs and their corresponding signatures over the data. + */ + public static List<Pair<Integer, byte[]>> generateSignaturesOverData( + SignerConfig signerConfig, byte[] data) + throws InvalidKeyException, NoSuchAlgorithmException, SignatureException { + List<Pair<Integer, byte[]>> signatures = + new ArrayList<>(signerConfig.signatureAlgorithms.size()); + PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey(); + for (SignatureAlgorithm signatureAlgorithm : signerConfig.signatureAlgorithms) { + Pair<String, ? extends AlgorithmParameterSpec> sigAlgAndParams = + signatureAlgorithm.getJcaSignatureAlgorithmAndParams(); + String jcaSignatureAlgorithm = sigAlgAndParams.getFirst(); + AlgorithmParameterSpec jcaSignatureAlgorithmParams = sigAlgAndParams.getSecond(); + byte[] signatureBytes; + try { + Signature signature = Signature.getInstance(jcaSignatureAlgorithm); + signature.initSign(signerConfig.privateKey); + if (jcaSignatureAlgorithmParams != null) { + signature.setParameter(jcaSignatureAlgorithmParams); + } + signature.update(data); + signatureBytes = signature.sign(); + } catch (InvalidKeyException e) { + throw new InvalidKeyException("Failed to sign using " + jcaSignatureAlgorithm, e); + } catch (InvalidAlgorithmParameterException | SignatureException e) { + throw new SignatureException("Failed to sign using " + jcaSignatureAlgorithm, e); + } + + try { + Signature signature = Signature.getInstance(jcaSignatureAlgorithm); + signature.initVerify(publicKey); + if (jcaSignatureAlgorithmParams != null) { + signature.setParameter(jcaSignatureAlgorithmParams); + } + signature.update(data); + if (!signature.verify(signatureBytes)) { + throw new SignatureException("Failed to verify generated " + + jcaSignatureAlgorithm + + " signature using public key from certificate"); + } + } catch (InvalidKeyException e) { + throw new InvalidKeyException( + "Failed to verify generated " + jcaSignatureAlgorithm + " signature using" + + " public key from certificate", e); + } catch (InvalidAlgorithmParameterException | SignatureException e) { + throw new SignatureException( + "Failed to verify generated " + jcaSignatureAlgorithm + " signature using" + + " public key from certificate", e); + } + + signatures.add(Pair.of(signatureAlgorithm.getId(), signatureBytes)); + } + return signatures; + } + + /** + * Wrap the signature according to CMS PKCS #7 RFC 5652. + * The high-level simplified structure is as follows: + * // ContentInfo + * // digestAlgorithm + * // SignedData + * // bag of certificates + * // SignerInfo + * // signing cert issuer and serial number (for locating the cert in the above bag) + * // digestAlgorithm + * // signatureAlgorithm + * // signature + * + * @throws Asn1EncodingException if the ASN.1 structure could not be encoded + */ + public static byte[] generatePkcs7DerEncodedMessage( + byte[] signatureBytes, ByteBuffer data, List<X509Certificate> signerCerts, + AlgorithmIdentifier digestAlgorithmId, AlgorithmIdentifier signatureAlgorithmId) + throws Asn1EncodingException, CertificateEncodingException { + SignerInfo signerInfo = new SignerInfo(); + signerInfo.version = 1; + X509Certificate signingCert = signerCerts.get(0); + X500Principal signerCertIssuer = signingCert.getIssuerX500Principal(); + signerInfo.sid = + new SignerIdentifier( + new IssuerAndSerialNumber( + new Asn1OpaqueObject(signerCertIssuer.getEncoded()), + signingCert.getSerialNumber())); + + signerInfo.digestAlgorithm = digestAlgorithmId; + signerInfo.signatureAlgorithm = signatureAlgorithmId; + signerInfo.signature = ByteBuffer.wrap(signatureBytes); + + SignedData signedData = new SignedData(); + signedData.certificates = new ArrayList<>(signerCerts.size()); + for (X509Certificate cert : signerCerts) { + signedData.certificates.add(new Asn1OpaqueObject(cert.getEncoded())); + } + signedData.version = 1; + signedData.digestAlgorithms = Collections.singletonList(digestAlgorithmId); + signedData.encapContentInfo = new EncapsulatedContentInfo(Pkcs7Constants.OID_DATA); + // If data is not null, data will be embedded as is in the result -- an attached pcsk7 + signedData.encapContentInfo.content = data; + signedData.signerInfos = Collections.singletonList(signerInfo); + ContentInfo contentInfo = new ContentInfo(); + contentInfo.contentType = Pkcs7Constants.OID_SIGNED_DATA; + contentInfo.content = new Asn1OpaqueObject(Asn1DerEncoder.encode(signedData)); + return Asn1DerEncoder.encode(contentInfo); + } + + /** + * Picks the correct v2/v3 digest for v4 signature verification. + * + * Keep in sync with pickBestDigestForV4 in framework's ApkSigningBlockUtils. + */ + public static byte[] pickBestDigestForV4(Map<ContentDigestAlgorithm, byte[]> contentDigests) { + for (ContentDigestAlgorithm algo : V4_CONTENT_DIGEST_ALGORITHMS) { + if (contentDigests.containsKey(algo)) { + return contentDigests.get(algo); + } + } + return null; + } + + /** + * Signer configuration. + */ + public static class SignerConfig { + /** Private key. */ + public PrivateKey privateKey; + + /** + * Certificates, with the first certificate containing the public key corresponding to + * {@link #privateKey}. + */ + public List<X509Certificate> certificates; + + /** + * List of signature algorithms with which to sign. + */ + public List<SignatureAlgorithm> signatureAlgorithms; + + public int minSdkVersion; + public int maxSdkVersion; + public boolean signerTargetsDevRelease; + public SigningCertificateLineage signingCertificateLineage; + } + + public static class Result extends ApkSigResult { + public SigningCertificateLineage signingCertificateLineage = null; + public final List<Result.SignerInfo> signers = new ArrayList<>(); + private final List<ApkVerifier.IssueWithParams> mWarnings = new ArrayList<>(); + private final List<ApkVerifier.IssueWithParams> mErrors = new ArrayList<>(); + + public Result(int signatureSchemeVersion) { + super(signatureSchemeVersion); + } + + public boolean containsErrors() { + if (!mErrors.isEmpty()) { + return true; + } + if (!signers.isEmpty()) { + for (Result.SignerInfo signer : signers) { + if (signer.containsErrors()) { + return true; + } + } + } + return false; + } + + public boolean containsWarnings() { + if (!mWarnings.isEmpty()) { + return true; + } + if (!signers.isEmpty()) { + for (Result.SignerInfo signer : signers) { + if (signer.containsWarnings()) { + return true; + } + } + } + return false; + } + + public void addError(ApkVerifier.Issue msg, Object... parameters) { + mErrors.add(new ApkVerifier.IssueWithParams(msg, parameters)); + } + + public void addWarning(ApkVerifier.Issue msg, Object... parameters) { + mWarnings.add(new ApkVerifier.IssueWithParams(msg, parameters)); + } + + @Override + public List<ApkVerifier.IssueWithParams> getErrors() { + return mErrors; + } + + @Override + public List<ApkVerifier.IssueWithParams> getWarnings() { + return mWarnings; + } + + public static class SignerInfo extends ApkSignerInfo { + public List<ContentDigest> contentDigests = new ArrayList<>(); + public Map<ContentDigestAlgorithm, byte[]> verifiedContentDigests = new HashMap<>(); + public List<Signature> signatures = new ArrayList<>(); + public Map<SignatureAlgorithm, byte[]> verifiedSignatures = new HashMap<>(); + public List<AdditionalAttribute> additionalAttributes = new ArrayList<>(); + public byte[] signedData; + public int minSdkVersion; + public int maxSdkVersion; + public SigningCertificateLineage signingCertificateLineage; + + private final List<ApkVerifier.IssueWithParams> mWarnings = new ArrayList<>(); + private final List<ApkVerifier.IssueWithParams> mErrors = new ArrayList<>(); + + public void addError(ApkVerifier.Issue msg, Object... parameters) { + mErrors.add(new ApkVerifier.IssueWithParams(msg, parameters)); + } + + public void addWarning(ApkVerifier.Issue msg, Object... parameters) { + mWarnings.add(new ApkVerifier.IssueWithParams(msg, parameters)); + } + + public boolean containsErrors() { + return !mErrors.isEmpty(); + } + + public boolean containsWarnings() { + return !mWarnings.isEmpty(); + } + + public List<ApkVerifier.IssueWithParams> getErrors() { + return mErrors; + } + + public List<ApkVerifier.IssueWithParams> getWarnings() { + return mWarnings; + } + + public static class ContentDigest { + private final int mSignatureAlgorithmId; + private final byte[] mValue; + + public ContentDigest(int signatureAlgorithmId, byte[] value) { + mSignatureAlgorithmId = signatureAlgorithmId; + mValue = value; + } + + public int getSignatureAlgorithmId() { + return mSignatureAlgorithmId; + } + + public byte[] getValue() { + return mValue; + } + } + + public static class Signature { + private final int mAlgorithmId; + private final byte[] mValue; + + public Signature(int algorithmId, byte[] value) { + mAlgorithmId = algorithmId; + mValue = value; + } + + public int getAlgorithmId() { + return mAlgorithmId; + } + + public byte[] getValue() { + return mValue; + } + } + + public static class AdditionalAttribute { + private final int mId; + private final byte[] mValue; + + public AdditionalAttribute(int id, byte[] value) { + mId = id; + mValue = value.clone(); + } + + public int getId() { + return mId; + } + + public byte[] getValue() { + return mValue.clone(); + } + } + } + } + + public static class SupportedSignature extends ApkSupportedSignature { + public SupportedSignature(SignatureAlgorithm algorithm, byte[] signature) { + super(algorithm, signature); + } + } + + public static class SigningSchemeBlockAndDigests { + public final Pair<byte[], Integer> signingSchemeBlock; + public final Map<ContentDigestAlgorithm, byte[]> digestInfo; + + public SigningSchemeBlockAndDigests( + Pair<byte[], Integer> signingSchemeBlock, + Map<ContentDigestAlgorithm, byte[]> digestInfo) { + this.signingSchemeBlock = signingSchemeBlock; + this.digestInfo = digestInfo; + } + } +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtilsLite.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtilsLite.java new file mode 100644 index 0000000000..40ae94798a --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtilsLite.java @@ -0,0 +1,393 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.apk; + +import com.android.apksig.apk.ApkFormatException; +import com.android.apksig.apk.ApkSigningBlockNotFoundException; +import com.android.apksig.apk.ApkUtilsLite; +import com.android.apksig.internal.util.Pair; +import com.android.apksig.util.DataSource; +import com.android.apksig.zip.ZipSections; + +import java.io.IOException; +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Lightweight version of the ApkSigningBlockUtils for clients that only require a subset of the + * utility functionality. + */ +public class ApkSigningBlockUtilsLite { + private ApkSigningBlockUtilsLite() {} + + private static final char[] HEX_DIGITS = "0123456789abcdef".toCharArray(); + /** + * Returns the APK Signature Scheme block contained in the provided APK file for the given ID + * and the additional information relevant for verifying the block against the file. + * + * @param blockId the ID value in the APK Signing Block's sequence of ID-value pairs + * identifying the appropriate block to find, e.g. the APK Signature Scheme v2 + * block ID. + * + * @throws SignatureNotFoundException if the APK is not signed using given APK Signature Scheme + * @throws IOException if an I/O error occurs while reading the APK + */ + public static SignatureInfo findSignature( + DataSource apk, ZipSections zipSections, int blockId) + throws IOException, SignatureNotFoundException { + // Find the APK Signing Block. + DataSource apkSigningBlock; + long apkSigningBlockOffset; + try { + ApkUtilsLite.ApkSigningBlock apkSigningBlockInfo = + ApkUtilsLite.findApkSigningBlock(apk, zipSections); + apkSigningBlockOffset = apkSigningBlockInfo.getStartOffset(); + apkSigningBlock = apkSigningBlockInfo.getContents(); + } catch (ApkSigningBlockNotFoundException e) { + throw new SignatureNotFoundException(e.getMessage(), e); + } + ByteBuffer apkSigningBlockBuf = + apkSigningBlock.getByteBuffer(0, (int) apkSigningBlock.size()); + apkSigningBlockBuf.order(ByteOrder.LITTLE_ENDIAN); + + // Find the APK Signature Scheme Block inside the APK Signing Block. + ByteBuffer apkSignatureSchemeBlock = + findApkSignatureSchemeBlock(apkSigningBlockBuf, blockId); + return new SignatureInfo( + apkSignatureSchemeBlock, + apkSigningBlockOffset, + zipSections.getZipCentralDirectoryOffset(), + zipSections.getZipEndOfCentralDirectoryOffset(), + zipSections.getZipEndOfCentralDirectory()); + } + + public static ByteBuffer findApkSignatureSchemeBlock( + ByteBuffer apkSigningBlock, + int blockId) throws SignatureNotFoundException { + checkByteOrderLittleEndian(apkSigningBlock); + // FORMAT: + // OFFSET DATA TYPE DESCRIPTION + // * @+0 bytes uint64: size in bytes (excluding this field) + // * @+8 bytes pairs + // * @-24 bytes uint64: size in bytes (same as the one above) + // * @-16 bytes uint128: magic + ByteBuffer pairs = sliceFromTo(apkSigningBlock, 8, apkSigningBlock.capacity() - 24); + + int entryCount = 0; + while (pairs.hasRemaining()) { + entryCount++; + if (pairs.remaining() < 8) { + throw new SignatureNotFoundException( + "Insufficient data to read size of APK Signing Block entry #" + entryCount); + } + long lenLong = pairs.getLong(); + if ((lenLong < 4) || (lenLong > Integer.MAX_VALUE)) { + throw new SignatureNotFoundException( + "APK Signing Block entry #" + entryCount + + " size out of range: " + lenLong); + } + int len = (int) lenLong; + int nextEntryPos = pairs.position() + len; + if (len > pairs.remaining()) { + throw new SignatureNotFoundException( + "APK Signing Block entry #" + entryCount + " size out of range: " + len + + ", available: " + pairs.remaining()); + } + int id = pairs.getInt(); + if (id == blockId) { + return getByteBuffer(pairs, len - 4); + } + pairs.position(nextEntryPos); + } + + throw new SignatureNotFoundException( + "No APK Signature Scheme block in APK Signing Block with ID: " + blockId); + } + + public static void checkByteOrderLittleEndian(ByteBuffer buffer) { + if (buffer.order() != ByteOrder.LITTLE_ENDIAN) { + throw new IllegalArgumentException("ByteBuffer byte order must be little endian"); + } + } + + /** + * Returns the subset of signatures which are expected to be verified by at least one Android + * platform version in the {@code [minSdkVersion, maxSdkVersion]} range. The returned result is + * guaranteed to contain at least one signature. + * + * <p>Each Android platform version typically verifies exactly one signature from the provided + * {@code signatures} set. This method returns the set of these signatures collected over all + * requested platform versions. As a result, the result may contain more than one signature. + * + * @throws NoApkSupportedSignaturesException if no supported signatures were + * found for an Android platform version in the range. + */ + public static <T extends ApkSupportedSignature> List<T> getSignaturesToVerify( + List<T> signatures, int minSdkVersion, int maxSdkVersion) + throws NoApkSupportedSignaturesException { + return getSignaturesToVerify(signatures, minSdkVersion, maxSdkVersion, false); + } + + /** + * Returns the subset of signatures which are expected to be verified by at least one Android + * platform version in the {@code [minSdkVersion, maxSdkVersion]} range. The returned result is + * guaranteed to contain at least one signature. + * + * <p>{@code onlyRequireJcaSupport} can be set to true for cases that only require verifying a + * signature within the signing block using the standard JCA. + * + * <p>Each Android platform version typically verifies exactly one signature from the provided + * {@code signatures} set. This method returns the set of these signatures collected over all + * requested platform versions. As a result, the result may contain more than one signature. + * + * @throws NoApkSupportedSignaturesException if no supported signatures were + * found for an Android platform version in the range. + */ + public static <T extends ApkSupportedSignature> List<T> getSignaturesToVerify( + List<T> signatures, int minSdkVersion, int maxSdkVersion, + boolean onlyRequireJcaSupport) throws + NoApkSupportedSignaturesException { + // Pick the signature with the strongest algorithm at all required SDK versions, to mimic + // Android's behavior on those versions. + // + // Here we assume that, once introduced, a signature algorithm continues to be supported in + // all future Android versions. We also assume that the better-than relationship between + // algorithms is exactly the same on all Android platform versions (except that older + // platforms might support fewer algorithms). If these assumption are no longer true, the + // logic here will need to change accordingly. + Map<Integer, T> + bestSigAlgorithmOnSdkVersion = new HashMap<>(); + int minProvidedSignaturesVersion = Integer.MAX_VALUE; + for (T sig : signatures) { + SignatureAlgorithm sigAlgorithm = sig.algorithm; + int sigMinSdkVersion = onlyRequireJcaSupport ? sigAlgorithm.getJcaSigAlgMinSdkVersion() + : sigAlgorithm.getMinSdkVersion(); + if (sigMinSdkVersion > maxSdkVersion) { + continue; + } + if (sigMinSdkVersion < minProvidedSignaturesVersion) { + minProvidedSignaturesVersion = sigMinSdkVersion; + } + + T candidate = bestSigAlgorithmOnSdkVersion.get(sigMinSdkVersion); + if ((candidate == null) + || (compareSignatureAlgorithm( + sigAlgorithm, candidate.algorithm) > 0)) { + bestSigAlgorithmOnSdkVersion.put(sigMinSdkVersion, sig); + } + } + + // Must have some supported signature algorithms for minSdkVersion. + if (minSdkVersion < minProvidedSignaturesVersion) { + throw new NoApkSupportedSignaturesException( + "Minimum provided signature version " + minProvidedSignaturesVersion + + " > minSdkVersion " + minSdkVersion); + } + if (bestSigAlgorithmOnSdkVersion.isEmpty()) { + throw new NoApkSupportedSignaturesException("No supported signature"); + } + List<T> signaturesToVerify = + new ArrayList<>(bestSigAlgorithmOnSdkVersion.values()); + Collections.sort( + signaturesToVerify, + (sig1, sig2) -> Integer.compare(sig1.algorithm.getId(), sig2.algorithm.getId())); + return signaturesToVerify; + } + + /** + * Returns positive number if {@code alg1} is preferred over {@code alg2}, {@code -1} if + * {@code alg2} is preferred over {@code alg1}, and {@code 0} if there is no preference. + */ + public static int compareSignatureAlgorithm(SignatureAlgorithm alg1, SignatureAlgorithm alg2) { + ContentDigestAlgorithm digestAlg1 = alg1.getContentDigestAlgorithm(); + ContentDigestAlgorithm digestAlg2 = alg2.getContentDigestAlgorithm(); + return compareContentDigestAlgorithm(digestAlg1, digestAlg2); + } + + /** + * Returns a positive number if {@code alg1} is preferred over {@code alg2}, a negative number + * if {@code alg2} is preferred over {@code alg1}, or {@code 0} if there is no preference. + */ + private static int compareContentDigestAlgorithm( + ContentDigestAlgorithm alg1, + ContentDigestAlgorithm alg2) { + switch (alg1) { + case CHUNKED_SHA256: + switch (alg2) { + case CHUNKED_SHA256: + return 0; + case CHUNKED_SHA512: + case VERITY_CHUNKED_SHA256: + return -1; + default: + throw new IllegalArgumentException("Unknown alg2: " + alg2); + } + case CHUNKED_SHA512: + switch (alg2) { + case CHUNKED_SHA256: + case VERITY_CHUNKED_SHA256: + return 1; + case CHUNKED_SHA512: + return 0; + default: + throw new IllegalArgumentException("Unknown alg2: " + alg2); + } + case VERITY_CHUNKED_SHA256: + switch (alg2) { + case CHUNKED_SHA256: + return 1; + case VERITY_CHUNKED_SHA256: + return 0; + case CHUNKED_SHA512: + return -1; + default: + throw new IllegalArgumentException("Unknown alg2: " + alg2); + } + default: + throw new IllegalArgumentException("Unknown alg1: " + alg1); + } + } + + /** + * Returns new byte buffer whose content is a shared subsequence of this buffer's content + * between the specified start (inclusive) and end (exclusive) positions. As opposed to + * {@link ByteBuffer#slice()}, the returned buffer's byte order is the same as the source + * buffer's byte order. + */ + private static ByteBuffer sliceFromTo(ByteBuffer source, int start, int end) { + if (start < 0) { + throw new IllegalArgumentException("start: " + start); + } + if (end < start) { + throw new IllegalArgumentException("end < start: " + end + " < " + start); + } + int capacity = source.capacity(); + if (end > source.capacity()) { + throw new IllegalArgumentException("end > capacity: " + end + " > " + capacity); + } + int originalLimit = source.limit(); + int originalPosition = source.position(); + try { + source.position(0); + source.limit(end); + source.position(start); + ByteBuffer result = source.slice(); + result.order(source.order()); + return result; + } finally { + source.position(0); + source.limit(originalLimit); + source.position(originalPosition); + } + } + + /** + * Relative <em>get</em> method for reading {@code size} number of bytes from the current + * position of this buffer. + * + * <p>This method reads the next {@code size} bytes at this buffer's current position, + * returning them as a {@code ByteBuffer} with start set to 0, limit and capacity set to + * {@code size}, byte order set to this buffer's byte order; and then increments the position by + * {@code size}. + */ + private static ByteBuffer getByteBuffer(ByteBuffer source, int size) { + if (size < 0) { + throw new IllegalArgumentException("size: " + size); + } + int originalLimit = source.limit(); + int position = source.position(); + int limit = position + size; + if ((limit < position) || (limit > originalLimit)) { + throw new BufferUnderflowException(); + } + source.limit(limit); + try { + ByteBuffer result = source.slice(); + result.order(source.order()); + source.position(limit); + return result; + } finally { + source.limit(originalLimit); + } + } + + public static String toHex(byte[] value) { + StringBuilder sb = new StringBuilder(value.length * 2); + int len = value.length; + for (int i = 0; i < len; i++) { + int hi = (value[i] & 0xff) >>> 4; + int lo = value[i] & 0x0f; + sb.append(HEX_DIGITS[hi]).append(HEX_DIGITS[lo]); + } + return sb.toString(); + } + + public static ByteBuffer getLengthPrefixedSlice(ByteBuffer source) throws ApkFormatException { + if (source.remaining() < 4) { + throw new ApkFormatException( + "Remaining buffer too short to contain length of length-prefixed field" + + ". Remaining: " + source.remaining()); + } + int len = source.getInt(); + if (len < 0) { + throw new IllegalArgumentException("Negative length"); + } else if (len > source.remaining()) { + throw new ApkFormatException( + "Length-prefixed field longer than remaining buffer" + + ". Field length: " + len + ", remaining: " + source.remaining()); + } + return getByteBuffer(source, len); + } + + public static byte[] readLengthPrefixedByteArray(ByteBuffer buf) throws ApkFormatException { + int len = buf.getInt(); + if (len < 0) { + throw new ApkFormatException("Negative length"); + } else if (len > buf.remaining()) { + throw new ApkFormatException( + "Underflow while reading length-prefixed value. Length: " + len + + ", available: " + buf.remaining()); + } + byte[] result = new byte[len]; + buf.get(result); + return result; + } + + public static byte[] encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes( + List<Pair<Integer, byte[]>> sequence) { + int resultSize = 0; + for (Pair<Integer, byte[]> element : sequence) { + resultSize += 12 + element.getSecond().length; + } + ByteBuffer result = ByteBuffer.allocate(resultSize); + result.order(ByteOrder.LITTLE_ENDIAN); + for (Pair<Integer, byte[]> element : sequence) { + byte[] second = element.getSecond(); + result.putInt(8 + second.length); + result.putInt(element.getFirst()); + result.putInt(second.length); + result.put(second); + } + return result.array(); + } +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/ApkSupportedSignature.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/ApkSupportedSignature.java new file mode 100644 index 0000000000..61652a435e --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/ApkSupportedSignature.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.apk; + +/** + * Base implementation of a supported signature for an APK. + */ +public class ApkSupportedSignature { + public final SignatureAlgorithm algorithm; + public final byte[] signature; + + /** + * Constructs a new supported signature using the provided {@code algorithm} and {@code + * signature} bytes. + */ + public ApkSupportedSignature(SignatureAlgorithm algorithm, byte[] signature) { + this.algorithm = algorithm; + this.signature = signature; + } + +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/ContentDigestAlgorithm.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/ContentDigestAlgorithm.java new file mode 100644 index 0000000000..b806d1e420 --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/ContentDigestAlgorithm.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.apk; + +/** APK Signature Scheme v2 content digest algorithm. */ +public enum ContentDigestAlgorithm { + /** SHA2-256 over 1 MB chunks. */ + CHUNKED_SHA256(1, "SHA-256", 256 / 8), + + /** SHA2-512 over 1 MB chunks. */ + CHUNKED_SHA512(2, "SHA-512", 512 / 8), + + /** SHA2-256 over 4 KB chunks for APK verity. */ + VERITY_CHUNKED_SHA256(3, "SHA-256", 256 / 8), + + /** Non-chunk SHA2-256. */ + SHA256(4, "SHA-256", 256 / 8); + + private final int mId; + private final String mJcaMessageDigestAlgorithm; + private final int mChunkDigestOutputSizeBytes; + + private ContentDigestAlgorithm( + int id, String jcaMessageDigestAlgorithm, int chunkDigestOutputSizeBytes) { + mId = id; + mJcaMessageDigestAlgorithm = jcaMessageDigestAlgorithm; + mChunkDigestOutputSizeBytes = chunkDigestOutputSizeBytes; + } + + /** Returns the ID of the digest algorithm used on the APK. */ + public int getId() { + return mId; + } + + /** + * Returns the {@link java.security.MessageDigest} algorithm used for computing digests of + * chunks by this content digest algorithm. + */ + String getJcaMessageDigestAlgorithm() { + return mJcaMessageDigestAlgorithm; + } + + /** Returns the size (in bytes) of the digest of a chunk of content. */ + int getChunkDigestOutputSizeBytes() { + return mChunkDigestOutputSizeBytes; + } +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/NoApkSupportedSignaturesException.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/NoApkSupportedSignaturesException.java new file mode 100644 index 0000000000..52c6085c5f --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/NoApkSupportedSignaturesException.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.apk; + +/** + * Base exception that is thrown when there are no signatures that support the full range of + * requested platform versions. + */ +public class NoApkSupportedSignaturesException extends Exception { + public NoApkSupportedSignaturesException(String message) { + super(message); + } +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/SignatureAlgorithm.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/SignatureAlgorithm.java new file mode 100644 index 0000000000..804eb37bdd --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/SignatureAlgorithm.java @@ -0,0 +1,225 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.apk; + +import com.android.apksig.internal.util.AndroidSdkVersion; +import com.android.apksig.internal.util.Pair; +import java.security.spec.AlgorithmParameterSpec; +import java.security.spec.MGF1ParameterSpec; +import java.security.spec.PSSParameterSpec; + +/** + * APK Signing Block signature algorithm. + */ +public enum SignatureAlgorithm { + // TODO reserve the 0x0000 ID to mean null + /** + * RSASSA-PSS with SHA2-256 digest, SHA2-256 MGF1, 32 bytes of salt, trailer: 0xbc, content + * digested using SHA2-256 in 1 MB chunks. + */ + RSA_PSS_WITH_SHA256( + 0x0101, + ContentDigestAlgorithm.CHUNKED_SHA256, + "RSA", + Pair.of("SHA256withRSA/PSS", + new PSSParameterSpec( + "SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 256 / 8, 1)), + AndroidSdkVersion.N, + AndroidSdkVersion.M), + + /** + * RSASSA-PSS with SHA2-512 digest, SHA2-512 MGF1, 64 bytes of salt, trailer: 0xbc, content + * digested using SHA2-512 in 1 MB chunks. + */ + RSA_PSS_WITH_SHA512( + 0x0102, + ContentDigestAlgorithm.CHUNKED_SHA512, + "RSA", + Pair.of( + "SHA512withRSA/PSS", + new PSSParameterSpec( + "SHA-512", "MGF1", MGF1ParameterSpec.SHA512, 512 / 8, 1)), + AndroidSdkVersion.N, + AndroidSdkVersion.M), + + /** RSASSA-PKCS1-v1_5 with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. */ + RSA_PKCS1_V1_5_WITH_SHA256( + 0x0103, + ContentDigestAlgorithm.CHUNKED_SHA256, + "RSA", + Pair.of("SHA256withRSA", null), + AndroidSdkVersion.N, + AndroidSdkVersion.INITIAL_RELEASE), + + /** RSASSA-PKCS1-v1_5 with SHA2-512 digest, content digested using SHA2-512 in 1 MB chunks. */ + RSA_PKCS1_V1_5_WITH_SHA512( + 0x0104, + ContentDigestAlgorithm.CHUNKED_SHA512, + "RSA", + Pair.of("SHA512withRSA", null), + AndroidSdkVersion.N, + AndroidSdkVersion.INITIAL_RELEASE), + + /** ECDSA with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. */ + ECDSA_WITH_SHA256( + 0x0201, + ContentDigestAlgorithm.CHUNKED_SHA256, + "EC", + Pair.of("SHA256withECDSA", null), + AndroidSdkVersion.N, + AndroidSdkVersion.HONEYCOMB), + + /** ECDSA with SHA2-512 digest, content digested using SHA2-512 in 1 MB chunks. */ + ECDSA_WITH_SHA512( + 0x0202, + ContentDigestAlgorithm.CHUNKED_SHA512, + "EC", + Pair.of("SHA512withECDSA", null), + AndroidSdkVersion.N, + AndroidSdkVersion.HONEYCOMB), + + /** DSA with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. */ + DSA_WITH_SHA256( + 0x0301, + ContentDigestAlgorithm.CHUNKED_SHA256, + "DSA", + Pair.of("SHA256withDSA", null), + AndroidSdkVersion.N, + AndroidSdkVersion.INITIAL_RELEASE), + + /** + * DSA with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. Signing is done + * deterministically according to RFC 6979. + */ + DETDSA_WITH_SHA256( + 0x0301, + ContentDigestAlgorithm.CHUNKED_SHA256, + "DSA", + Pair.of("SHA256withDetDSA", null), + AndroidSdkVersion.N, + AndroidSdkVersion.INITIAL_RELEASE), + + /** + * RSASSA-PKCS1-v1_5 with SHA2-256 digest, content digested using SHA2-256 in 4 KB chunks, in + * the same way fsverity operates. This digest and the content length (before digestion, 8 bytes + * in little endian) construct the final digest. + */ + VERITY_RSA_PKCS1_V1_5_WITH_SHA256( + 0x0421, + ContentDigestAlgorithm.VERITY_CHUNKED_SHA256, + "RSA", + Pair.of("SHA256withRSA", null), + AndroidSdkVersion.P, + AndroidSdkVersion.INITIAL_RELEASE), + + /** + * ECDSA with SHA2-256 digest, content digested using SHA2-256 in 4 KB chunks, in the same way + * fsverity operates. This digest and the content length (before digestion, 8 bytes in little + * endian) construct the final digest. + */ + VERITY_ECDSA_WITH_SHA256( + 0x0423, + ContentDigestAlgorithm.VERITY_CHUNKED_SHA256, + "EC", + Pair.of("SHA256withECDSA", null), + AndroidSdkVersion.P, + AndroidSdkVersion.HONEYCOMB), + + /** + * DSA with SHA2-256 digest, content digested using SHA2-256 in 4 KB chunks, in the same way + * fsverity operates. This digest and the content length (before digestion, 8 bytes in little + * endian) construct the final digest. + */ + VERITY_DSA_WITH_SHA256( + 0x0425, + ContentDigestAlgorithm.VERITY_CHUNKED_SHA256, + "DSA", + Pair.of("SHA256withDSA", null), + AndroidSdkVersion.P, + AndroidSdkVersion.INITIAL_RELEASE); + + private final int mId; + private final String mJcaKeyAlgorithm; + private final ContentDigestAlgorithm mContentDigestAlgorithm; + private final Pair<String, ? extends AlgorithmParameterSpec> mJcaSignatureAlgAndParams; + private final int mMinSdkVersion; + private final int mJcaSigAlgMinSdkVersion; + + SignatureAlgorithm(int id, + ContentDigestAlgorithm contentDigestAlgorithm, + String jcaKeyAlgorithm, + Pair<String, ? extends AlgorithmParameterSpec> jcaSignatureAlgAndParams, + int minSdkVersion, + int jcaSigAlgMinSdkVersion) { + mId = id; + mContentDigestAlgorithm = contentDigestAlgorithm; + mJcaKeyAlgorithm = jcaKeyAlgorithm; + mJcaSignatureAlgAndParams = jcaSignatureAlgAndParams; + mMinSdkVersion = minSdkVersion; + mJcaSigAlgMinSdkVersion = jcaSigAlgMinSdkVersion; + } + + /** + * Returns the ID of this signature algorithm as used in APK Signature Scheme v2 wire format. + */ + public int getId() { + return mId; + } + + /** + * Returns the content digest algorithm associated with this signature algorithm. + */ + public ContentDigestAlgorithm getContentDigestAlgorithm() { + return mContentDigestAlgorithm; + } + + /** + * Returns the JCA {@link java.security.Key} algorithm used by this signature scheme. + */ + public String getJcaKeyAlgorithm() { + return mJcaKeyAlgorithm; + } + + /** + * Returns the {@link java.security.Signature} algorithm and the {@link AlgorithmParameterSpec} + * (or null if not needed) to parameterize the {@code Signature}. + */ + public Pair<String, ? extends AlgorithmParameterSpec> getJcaSignatureAlgorithmAndParams() { + return mJcaSignatureAlgAndParams; + } + + public int getMinSdkVersion() { + return mMinSdkVersion; + } + + /** + * Returns the minimum SDK version that supports the JCA signature algorithm. + */ + public int getJcaSigAlgMinSdkVersion() { + return mJcaSigAlgMinSdkVersion; + } + + public static SignatureAlgorithm findById(int id) { + for (SignatureAlgorithm alg : SignatureAlgorithm.values()) { + if (alg.getId() == id) { + return alg; + } + } + + return null; + } +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/SignatureInfo.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/SignatureInfo.java new file mode 100644 index 0000000000..5e26327b8d --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/SignatureInfo.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.apk; + +import java.nio.ByteBuffer; + +/** + * APK Signature Scheme block and additional information relevant to verifying the signatures + * contained in the block against the file. + */ +public class SignatureInfo { + /** Contents of APK Signature Scheme block. */ + public final ByteBuffer signatureBlock; + + /** Position of the APK Signing Block in the file. */ + public final long apkSigningBlockOffset; + + /** Position of the ZIP Central Directory in the file. */ + public final long centralDirOffset; + + /** Position of the ZIP End of Central Directory (EoCD) in the file. */ + public final long eocdOffset; + + /** Contents of ZIP End of Central Directory (EoCD) of the file. */ + public final ByteBuffer eocd; + + public SignatureInfo( + ByteBuffer signatureBlock, + long apkSigningBlockOffset, + long centralDirOffset, + long eocdOffset, + ByteBuffer eocd) { + this.signatureBlock = signatureBlock; + this.apkSigningBlockOffset = apkSigningBlockOffset; + this.centralDirOffset = centralDirOffset; + this.eocdOffset = eocdOffset; + this.eocd = eocd; + } +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/SignatureNotFoundException.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/SignatureNotFoundException.java new file mode 100644 index 0000000000..95f06eff8a --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/SignatureNotFoundException.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.apk; + +/** + * Base exception that is thrown when the APK is not signed with the requested signature scheme. + */ +public class SignatureNotFoundException extends Exception { + public SignatureNotFoundException(String message) { + super(message); + } + + public SignatureNotFoundException(String message, Throwable cause) { + super(message, cause); + } +}
\ No newline at end of file diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampCertificateLineage.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampCertificateLineage.java new file mode 100644 index 0000000000..93627ff0e3 --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampCertificateLineage.java @@ -0,0 +1,235 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.apk.stamp; + +import static com.android.apksig.internal.apk.ApkSigningBlockUtilsLite.getLengthPrefixedSlice; +import static com.android.apksig.internal.apk.ApkSigningBlockUtilsLite.readLengthPrefixedByteArray; + +import com.android.apksig.apk.ApkFormatException; +import com.android.apksig.internal.apk.ApkSigningBlockUtilsLite; +import com.android.apksig.internal.apk.SignatureAlgorithm; +import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.Signature; +import java.security.SignatureException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.security.spec.AlgorithmParameterSpec; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; + +/** Lightweight version of the V3SigningCertificateLineage to be used for source stamps. */ +public class SourceStampCertificateLineage { + + private final static int FIRST_VERSION = 1; + private final static int CURRENT_VERSION = FIRST_VERSION; + + /** + * Deserializes the binary representation of a SourceStampCertificateLineage. Also + * verifies that the structure is well-formed, e.g. that the signature for each node is from its + * parent. + */ + public static List<SigningCertificateNode> readSigningCertificateLineage(ByteBuffer inputBytes) + throws IOException { + List<SigningCertificateNode> result = new ArrayList<>(); + int nodeCount = 0; + if (inputBytes == null || !inputBytes.hasRemaining()) { + return null; + } + + ApkSigningBlockUtilsLite.checkByteOrderLittleEndian(inputBytes); + + CertificateFactory certFactory; + try { + certFactory = CertificateFactory.getInstance("X.509"); + } catch (CertificateException e) { + throw new IllegalStateException("Failed to obtain X.509 CertificateFactory", e); + } + + // FORMAT (little endian): + // * uint32: version code + // * sequence of length-prefixed (uint32): nodes + // * length-prefixed bytes: signed data + // * length-prefixed bytes: certificate + // * uint32: signature algorithm id + // * uint32: flags + // * uint32: signature algorithm id (used by to sign next cert in lineage) + // * length-prefixed bytes: signature over above signed data + + X509Certificate lastCert = null; + int lastSigAlgorithmId = 0; + + try { + int version = inputBytes.getInt(); + if (version != CURRENT_VERSION) { + // we only have one version to worry about right now, so just check it + throw new IllegalArgumentException("Encoded SigningCertificateLineage has a version" + + " different than any of which we are aware"); + } + HashSet<X509Certificate> certHistorySet = new HashSet<>(); + while (inputBytes.hasRemaining()) { + nodeCount++; + ByteBuffer nodeBytes = getLengthPrefixedSlice(inputBytes); + ByteBuffer signedData = getLengthPrefixedSlice(nodeBytes); + int flags = nodeBytes.getInt(); + int sigAlgorithmId = nodeBytes.getInt(); + SignatureAlgorithm sigAlgorithm = SignatureAlgorithm.findById(lastSigAlgorithmId); + byte[] signature = readLengthPrefixedByteArray(nodeBytes); + + if (lastCert != null) { + // Use previous level cert to verify current level + String jcaSignatureAlgorithm = + sigAlgorithm.getJcaSignatureAlgorithmAndParams().getFirst(); + AlgorithmParameterSpec jcaSignatureAlgorithmParams = + sigAlgorithm.getJcaSignatureAlgorithmAndParams().getSecond(); + PublicKey publicKey = lastCert.getPublicKey(); + Signature sig = Signature.getInstance(jcaSignatureAlgorithm); + sig.initVerify(publicKey); + if (jcaSignatureAlgorithmParams != null) { + sig.setParameter(jcaSignatureAlgorithmParams); + } + sig.update(signedData); + if (!sig.verify(signature)) { + throw new SecurityException("Unable to verify signature of certificate #" + + nodeCount + " using " + jcaSignatureAlgorithm + " when verifying" + + " SourceStampCertificateLineage object"); + } + } + + signedData.rewind(); + byte[] encodedCert = readLengthPrefixedByteArray(signedData); + int signedSigAlgorithm = signedData.getInt(); + if (lastCert != null && lastSigAlgorithmId != signedSigAlgorithm) { + throw new SecurityException("Signing algorithm ID mismatch for certificate #" + + nodeBytes + " when verifying SourceStampCertificateLineage object"); + } + lastCert = (X509Certificate) certFactory.generateCertificate( + new ByteArrayInputStream(encodedCert)); + lastCert = new GuaranteedEncodedFormX509Certificate(lastCert, encodedCert); + if (certHistorySet.contains(lastCert)) { + throw new SecurityException("Encountered duplicate entries in " + + "SigningCertificateLineage at certificate #" + nodeCount + ". All " + + "signing certificates should be unique"); + } + certHistorySet.add(lastCert); + lastSigAlgorithmId = sigAlgorithmId; + result.add(new SigningCertificateNode( + lastCert, SignatureAlgorithm.findById(signedSigAlgorithm), + SignatureAlgorithm.findById(sigAlgorithmId), signature, flags)); + } + } catch(ApkFormatException | BufferUnderflowException e){ + throw new IOException("Failed to parse SourceStampCertificateLineage object", e); + } catch(NoSuchAlgorithmException | InvalidKeyException + | InvalidAlgorithmParameterException | SignatureException e){ + throw new SecurityException( + "Failed to verify signature over signed data for certificate #" + nodeCount + + " when parsing SourceStampCertificateLineage object", e); + } catch(CertificateException e){ + throw new SecurityException("Failed to decode certificate #" + nodeCount + + " when parsing SourceStampCertificateLineage object", e); + } + return result; + } + + /** + * Represents one signing certificate in the SourceStampCertificateLineage, which + * generally means it is/was used at some point to sign source stamps. + */ + public static class SigningCertificateNode { + + public SigningCertificateNode( + X509Certificate signingCert, + SignatureAlgorithm parentSigAlgorithm, + SignatureAlgorithm sigAlgorithm, + byte[] signature, + int flags) { + this.signingCert = signingCert; + this.parentSigAlgorithm = parentSigAlgorithm; + this.sigAlgorithm = sigAlgorithm; + this.signature = signature; + this.flags = flags; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof SigningCertificateNode)) return false; + + SigningCertificateNode that = (SigningCertificateNode) o; + if (!signingCert.equals(that.signingCert)) return false; + if (parentSigAlgorithm != that.parentSigAlgorithm) return false; + if (sigAlgorithm != that.sigAlgorithm) return false; + if (!Arrays.equals(signature, that.signature)) return false; + if (flags != that.flags) return false; + + // we made it + return true; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((signingCert == null) ? 0 : signingCert.hashCode()); + result = prime * result + + ((parentSigAlgorithm == null) ? 0 : parentSigAlgorithm.hashCode()); + result = prime * result + ((sigAlgorithm == null) ? 0 : sigAlgorithm.hashCode()); + result = prime * result + Arrays.hashCode(signature); + result = prime * result + flags; + return result; + } + + /** + * the signing cert for this node. This is part of the data signed by the parent node. + */ + public final X509Certificate signingCert; + + /** + * the algorithm used by this node's parent to bless this data. Its ID value is part of + * the data signed by the parent node. {@code null} for first node. + */ + public final SignatureAlgorithm parentSigAlgorithm; + + /** + * the algorithm used by this node to bless the next node's data. Its ID value is part + * of the signed data of the next node. {@code null} for the last node. + */ + public SignatureAlgorithm sigAlgorithm; + + /** + * signature over the signed data (above). The signature is from this node's parent + * signing certificate, which should correspond to the signing certificate used to sign an + * APK before rotating to this one, and is formed using {@code signatureAlgorithm}. + */ + public final byte[] signature; + + /** + * the flags detailing how the platform should treat this signing cert + */ + public int flags; + } +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampConstants.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampConstants.java new file mode 100644 index 0000000000..2a949adb71 --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampConstants.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.apk.stamp; + +/** Constants used for source stamp signing and verification. */ +public class SourceStampConstants { + private SourceStampConstants() {} + + public static final int V1_SOURCE_STAMP_BLOCK_ID = 0x2b09189e; + public static final int V2_SOURCE_STAMP_BLOCK_ID = 0x6dff800d; + public static final String SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME = "stamp-cert-sha256"; + public static final int PROOF_OF_ROTATION_ATTR_ID = 0x9d6303f7; + /** + * The source stamp timestamp attribute value is an 8-byte little-endian encoded long + * representing the epoch time in seconds when the stamp block was signed. The first 8 bytes + * of the attribute value buffer will be used to read the timestamp, and any additional buffer + * space will be ignored. + */ + public static final int STAMP_TIME_ATTR_ID = 0xe43c5946; +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampVerifier.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampVerifier.java new file mode 100644 index 0000000000..ef6da2f68f --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampVerifier.java @@ -0,0 +1,364 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.apksig.internal.apk.stamp; + +import static com.android.apksig.internal.apk.ApkSigningBlockUtilsLite.getLengthPrefixedSlice; +import static com.android.apksig.internal.apk.ApkSigningBlockUtilsLite.getSignaturesToVerify; +import static com.android.apksig.internal.apk.ApkSigningBlockUtilsLite.readLengthPrefixedByteArray; +import static com.android.apksig.internal.apk.ApkSigningBlockUtilsLite.toHex; + +import com.android.apksig.ApkVerificationIssue; +import com.android.apksig.Constants; +import com.android.apksig.apk.ApkFormatException; +import com.android.apksig.internal.apk.ApkSignerInfo; +import com.android.apksig.internal.apk.ApkSupportedSignature; +import com.android.apksig.internal.apk.NoApkSupportedSignaturesException; +import com.android.apksig.internal.apk.SignatureAlgorithm; +import com.android.apksig.internal.util.ByteBufferUtils; +import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate; + +import java.io.ByteArrayInputStream; +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.Signature; +import java.security.SignatureException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.security.spec.AlgorithmParameterSpec; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Source Stamp verifier. + * + * <p>SourceStamp improves traceability of apps with respect to unauthorized distribution. + * + * <p>The stamp is part of the APK that is protected by the signing block. + * + * <p>The APK contents hash is signed using the stamp key, and is saved as part of the signing + * block. + */ +class SourceStampVerifier { + /** Hidden constructor to prevent instantiation. */ + private SourceStampVerifier() { + } + + /** + * Parses the SourceStamp block and populates the {@code result}. + * + * <p>This verifies signatures over digest provided. + * + * <p>This method adds one or more errors to the {@code result} if a verification error is + * expected to be encountered on an Android platform version in the {@code [minSdkVersion, + * maxSdkVersion]} range. + */ + public static void verifyV1SourceStamp( + ByteBuffer sourceStampBlockData, + CertificateFactory certFactory, + ApkSignerInfo result, + byte[] apkDigest, + byte[] sourceStampCertificateDigest, + int minSdkVersion, + int maxSdkVersion) + throws ApkFormatException, NoSuchAlgorithmException { + X509Certificate sourceStampCertificate = + verifySourceStampCertificate( + sourceStampBlockData, certFactory, sourceStampCertificateDigest, result); + if (result.containsWarnings() || result.containsErrors()) { + return; + } + + ByteBuffer apkDigestSignatures = getLengthPrefixedSlice(sourceStampBlockData); + verifySourceStampSignature( + apkDigest, + minSdkVersion, + maxSdkVersion, + sourceStampCertificate, + apkDigestSignatures, + result); + } + + /** + * Parses the SourceStamp block and populates the {@code result}. + * + * <p>This verifies signatures over digest of multiple signature schemes provided. + * + * <p>This method adds one or more errors to the {@code result} if a verification error is + * expected to be encountered on an Android platform version in the {@code [minSdkVersion, + * maxSdkVersion]} range. + */ + public static void verifyV2SourceStamp( + ByteBuffer sourceStampBlockData, + CertificateFactory certFactory, + ApkSignerInfo result, + Map<Integer, byte[]> signatureSchemeApkDigests, + byte[] sourceStampCertificateDigest, + int minSdkVersion, + int maxSdkVersion) + throws ApkFormatException, NoSuchAlgorithmException { + X509Certificate sourceStampCertificate = + verifySourceStampCertificate( + sourceStampBlockData, certFactory, sourceStampCertificateDigest, result); + if (result.containsWarnings() || result.containsErrors()) { + return; + } + + // Parse signed signature schemes block. + ByteBuffer signedSignatureSchemes = getLengthPrefixedSlice(sourceStampBlockData); + Map<Integer, ByteBuffer> signedSignatureSchemeData = new HashMap<>(); + while (signedSignatureSchemes.hasRemaining()) { + ByteBuffer signedSignatureScheme = getLengthPrefixedSlice(signedSignatureSchemes); + int signatureSchemeId = signedSignatureScheme.getInt(); + ByteBuffer apkDigestSignatures = getLengthPrefixedSlice(signedSignatureScheme); + signedSignatureSchemeData.put(signatureSchemeId, apkDigestSignatures); + } + + for (Map.Entry<Integer, byte[]> signatureSchemeApkDigest : + signatureSchemeApkDigests.entrySet()) { + // TODO(b/192301300): Should the new v3.1 be included in the source stamp, or since a + // v3.0 block must always be present with a v3.1 block is it sufficient to just use the + // v3.0 block? + if (signatureSchemeApkDigest.getKey() + == Constants.VERSION_APK_SIGNATURE_SCHEME_V31) { + continue; + } + if (!signedSignatureSchemeData.containsKey(signatureSchemeApkDigest.getKey())) { + result.addWarning(ApkVerificationIssue.SOURCE_STAMP_NO_SIGNATURE); + return; + } + verifySourceStampSignature( + signatureSchemeApkDigest.getValue(), + minSdkVersion, + maxSdkVersion, + sourceStampCertificate, + signedSignatureSchemeData.get(signatureSchemeApkDigest.getKey()), + result); + if (result.containsWarnings() || result.containsErrors()) { + return; + } + } + + if (sourceStampBlockData.hasRemaining()) { + // The stamp block contains some additional attributes. + ByteBuffer stampAttributeData = getLengthPrefixedSlice(sourceStampBlockData); + ByteBuffer stampAttributeDataSignatures = getLengthPrefixedSlice(sourceStampBlockData); + + byte[] stampAttributeBytes = new byte[stampAttributeData.remaining()]; + stampAttributeData.get(stampAttributeBytes); + stampAttributeData.flip(); + + verifySourceStampSignature(stampAttributeBytes, minSdkVersion, maxSdkVersion, + sourceStampCertificate, stampAttributeDataSignatures, result); + if (result.containsErrors() || result.containsWarnings()) { + return; + } + parseStampAttributes(stampAttributeData, sourceStampCertificate, result); + } + } + + private static X509Certificate verifySourceStampCertificate( + ByteBuffer sourceStampBlockData, + CertificateFactory certFactory, + byte[] sourceStampCertificateDigest, + ApkSignerInfo result) + throws NoSuchAlgorithmException, ApkFormatException { + // Parse the SourceStamp certificate. + byte[] sourceStampEncodedCertificate = readLengthPrefixedByteArray(sourceStampBlockData); + X509Certificate sourceStampCertificate; + try { + sourceStampCertificate = (X509Certificate) certFactory.generateCertificate( + new ByteArrayInputStream(sourceStampEncodedCertificate)); + } catch (CertificateException e) { + result.addWarning(ApkVerificationIssue.SOURCE_STAMP_MALFORMED_CERTIFICATE, e); + return null; + } + // Wrap the cert so that the result's getEncoded returns exactly the original encoded + // form. Without this, getEncoded may return a different form from what was stored in + // the signature. This is because some X509Certificate(Factory) implementations + // re-encode certificates. + sourceStampCertificate = + new GuaranteedEncodedFormX509Certificate( + sourceStampCertificate, sourceStampEncodedCertificate); + result.certs.add(sourceStampCertificate); + // Verify the SourceStamp certificate found in the signing block is the same as the + // SourceStamp certificate found in the APK. + MessageDigest messageDigest = MessageDigest.getInstance("SHA-256"); + messageDigest.update(sourceStampEncodedCertificate); + byte[] sourceStampBlockCertificateDigest = messageDigest.digest(); + if (!Arrays.equals(sourceStampCertificateDigest, sourceStampBlockCertificateDigest)) { + result.addWarning( + ApkVerificationIssue + .SOURCE_STAMP_CERTIFICATE_MISMATCH_BETWEEN_SIGNATURE_BLOCK_AND_APK, + toHex(sourceStampBlockCertificateDigest), + toHex(sourceStampCertificateDigest)); + return null; + } + return sourceStampCertificate; + } + + private static void verifySourceStampSignature( + byte[] data, + int minSdkVersion, + int maxSdkVersion, + X509Certificate sourceStampCertificate, + ByteBuffer signatures, + ApkSignerInfo result) { + // Parse the signatures block and identify supported signatures + int signatureCount = 0; + List<ApkSupportedSignature> supportedSignatures = new ArrayList<>(1); + while (signatures.hasRemaining()) { + signatureCount++; + try { + ByteBuffer signature = getLengthPrefixedSlice(signatures); + int sigAlgorithmId = signature.getInt(); + byte[] sigBytes = readLengthPrefixedByteArray(signature); + SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.findById(sigAlgorithmId); + if (signatureAlgorithm == null) { + result.addInfoMessage( + ApkVerificationIssue.SOURCE_STAMP_UNKNOWN_SIG_ALGORITHM, + sigAlgorithmId); + continue; + } + supportedSignatures.add( + new ApkSupportedSignature(signatureAlgorithm, sigBytes)); + } catch (ApkFormatException | BufferUnderflowException e) { + result.addWarning( + ApkVerificationIssue.SOURCE_STAMP_MALFORMED_SIGNATURE, signatureCount); + return; + } + } + if (supportedSignatures.isEmpty()) { + result.addWarning(ApkVerificationIssue.SOURCE_STAMP_NO_SIGNATURE); + return; + } + // Verify signatures over digests using the SourceStamp's certificate. + List<ApkSupportedSignature> signaturesToVerify; + try { + signaturesToVerify = + getSignaturesToVerify( + supportedSignatures, minSdkVersion, maxSdkVersion, true); + } catch (NoApkSupportedSignaturesException e) { + // To facilitate debugging capture the signature algorithms and resulting exception in + // the warning. + StringBuilder signatureAlgorithms = new StringBuilder(); + for (ApkSupportedSignature supportedSignature : supportedSignatures) { + if (signatureAlgorithms.length() > 0) { + signatureAlgorithms.append(", "); + } + signatureAlgorithms.append(supportedSignature.algorithm); + } + result.addWarning(ApkVerificationIssue.SOURCE_STAMP_NO_SUPPORTED_SIGNATURE, + signatureAlgorithms.toString(), e); + return; + } + for (ApkSupportedSignature signature : signaturesToVerify) { + SignatureAlgorithm signatureAlgorithm = signature.algorithm; + String jcaSignatureAlgorithm = + signatureAlgorithm.getJcaSignatureAlgorithmAndParams().getFirst(); + AlgorithmParameterSpec jcaSignatureAlgorithmParams = + signatureAlgorithm.getJcaSignatureAlgorithmAndParams().getSecond(); + PublicKey publicKey = sourceStampCertificate.getPublicKey(); + try { + Signature sig = Signature.getInstance(jcaSignatureAlgorithm); + sig.initVerify(publicKey); + if (jcaSignatureAlgorithmParams != null) { + sig.setParameter(jcaSignatureAlgorithmParams); + } + sig.update(data); + byte[] sigBytes = signature.signature; + if (!sig.verify(sigBytes)) { + result.addWarning( + ApkVerificationIssue.SOURCE_STAMP_DID_NOT_VERIFY, signatureAlgorithm); + return; + } + } catch (InvalidKeyException + | InvalidAlgorithmParameterException + | SignatureException + | NoSuchAlgorithmException e) { + result.addWarning( + ApkVerificationIssue.SOURCE_STAMP_VERIFY_EXCEPTION, signatureAlgorithm, e); + return; + } + } + } + + private static void parseStampAttributes(ByteBuffer stampAttributeData, + X509Certificate sourceStampCertificate, ApkSignerInfo result) + throws ApkFormatException { + ByteBuffer stampAttributes = getLengthPrefixedSlice(stampAttributeData); + int stampAttributeCount = 0; + while (stampAttributes.hasRemaining()) { + stampAttributeCount++; + try { + ByteBuffer attribute = getLengthPrefixedSlice(stampAttributes); + int id = attribute.getInt(); + byte[] value = ByteBufferUtils.toByteArray(attribute); + if (id == SourceStampConstants.PROOF_OF_ROTATION_ATTR_ID) { + readStampCertificateLineage(value, sourceStampCertificate, result); + } else if (id == SourceStampConstants.STAMP_TIME_ATTR_ID) { + long timestamp = ByteBuffer.wrap(value).order( + ByteOrder.LITTLE_ENDIAN).getLong(); + if (timestamp > 0) { + result.timestamp = timestamp; + } else { + result.addWarning(ApkVerificationIssue.SOURCE_STAMP_INVALID_TIMESTAMP, + timestamp); + } + } else { + result.addInfoMessage(ApkVerificationIssue.SOURCE_STAMP_UNKNOWN_ATTRIBUTE, id); + } + } catch (ApkFormatException | BufferUnderflowException e) { + result.addWarning(ApkVerificationIssue.SOURCE_STAMP_MALFORMED_ATTRIBUTE, + stampAttributeCount); + return; + } + } + } + + private static void readStampCertificateLineage(byte[] lineageBytes, + X509Certificate sourceStampCertificate, ApkSignerInfo result) { + try { + // SourceStampCertificateLineage is verified when built + List<SourceStampCertificateLineage.SigningCertificateNode> nodes = + SourceStampCertificateLineage.readSigningCertificateLineage( + ByteBuffer.wrap(lineageBytes).order(ByteOrder.LITTLE_ENDIAN)); + for (int i = 0; i < nodes.size(); i++) { + result.certificateLineage.add(nodes.get(i).signingCert); + } + // Make sure that the last cert in the chain matches this signer cert + if (!sourceStampCertificate.equals( + result.certificateLineage.get(result.certificateLineage.size() - 1))) { + result.addWarning(ApkVerificationIssue.SOURCE_STAMP_POR_CERT_MISMATCH); + } + } catch (SecurityException e) { + result.addWarning(ApkVerificationIssue.SOURCE_STAMP_POR_DID_NOT_VERIFY); + } catch (IllegalArgumentException e) { + result.addWarning(ApkVerificationIssue.SOURCE_STAMP_POR_CERT_MISMATCH); + } catch (Exception e) { + result.addWarning(ApkVerificationIssue.SOURCE_STAMP_MALFORMED_LINEAGE); + } + } +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/stamp/V1SourceStampSigner.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/stamp/V1SourceStampSigner.java new file mode 100644 index 0000000000..dee24bd1f6 --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/stamp/V1SourceStampSigner.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.apk.stamp; + +import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsLengthPrefixedElement; +import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedElements; +import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes; + +import com.android.apksig.internal.apk.ApkSigningBlockUtils; +import com.android.apksig.internal.apk.ApkSigningBlockUtils.SignerConfig; +import com.android.apksig.internal.apk.ContentDigestAlgorithm; +import com.android.apksig.internal.util.Pair; + +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.SignatureException; +import java.security.cert.CertificateEncodingException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; + +/** + * SourceStamp signer. + * + * <p>SourceStamp improves traceability of apps with respect to unauthorized distribution. + * + * <p>The stamp is part of the APK that is protected by the signing block. + * + * <p>The APK contents hash is signed using the stamp key, and is saved as part of the signing + * block. + * + * <p>V1 of the source stamp allows signing the digest of at most one signature scheme only. + */ +public abstract class V1SourceStampSigner { + public static final int V1_SOURCE_STAMP_BLOCK_ID = + SourceStampConstants.V1_SOURCE_STAMP_BLOCK_ID; + + /** Hidden constructor to prevent instantiation. */ + private V1SourceStampSigner() {} + + public static Pair<byte[], Integer> generateSourceStampBlock( + SignerConfig sourceStampSignerConfig, Map<ContentDigestAlgorithm, byte[]> digestInfo) + throws SignatureException, NoSuchAlgorithmException, InvalidKeyException { + if (sourceStampSignerConfig.certificates.isEmpty()) { + throw new SignatureException("No certificates configured for signer"); + } + + List<Pair<Integer, byte[]>> digests = new ArrayList<>(); + for (Map.Entry<ContentDigestAlgorithm, byte[]> digest : digestInfo.entrySet()) { + digests.add(Pair.of(digest.getKey().getId(), digest.getValue())); + } + Collections.sort(digests, Comparator.comparing(Pair::getFirst)); + + SourceStampBlock sourceStampBlock = new SourceStampBlock(); + + try { + sourceStampBlock.stampCertificate = + sourceStampSignerConfig.certificates.get(0).getEncoded(); + } catch (CertificateEncodingException e) { + throw new SignatureException( + "Retrieving the encoded form of the stamp certificate failed", e); + } + + byte[] digestBytes = + encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(digests); + sourceStampBlock.signedDigests = + ApkSigningBlockUtils.generateSignaturesOverData( + sourceStampSignerConfig, digestBytes); + + // FORMAT: + // * length-prefixed bytes: X.509 certificate (ASN.1 DER encoded) + // * length-prefixed sequence of length-prefixed signatures: + // * uint32: signature algorithm ID + // * length-prefixed bytes: signature of signed data + byte[] sourceStampSignerBlock = + encodeAsSequenceOfLengthPrefixedElements( + new byte[][] { + sourceStampBlock.stampCertificate, + encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes( + sourceStampBlock.signedDigests), + }); + + // FORMAT: + // * length-prefixed stamp block. + return Pair.of(encodeAsLengthPrefixedElement(sourceStampSignerBlock), + SourceStampConstants.V1_SOURCE_STAMP_BLOCK_ID); + } + + private static final class SourceStampBlock { + public byte[] stampCertificate; + public List<Pair<Integer, byte[]>> signedDigests; + } +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/stamp/V1SourceStampVerifier.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/stamp/V1SourceStampVerifier.java new file mode 100644 index 0000000000..c3fdeecc7b --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/stamp/V1SourceStampVerifier.java @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.apksig.internal.apk.stamp; + +import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes; +import static com.android.apksig.internal.apk.stamp.SourceStampConstants.V1_SOURCE_STAMP_BLOCK_ID; + +import com.android.apksig.ApkVerifier; +import com.android.apksig.apk.ApkFormatException; +import com.android.apksig.apk.ApkUtils; +import com.android.apksig.internal.apk.ApkSigningBlockUtils; +import com.android.apksig.internal.apk.ContentDigestAlgorithm; +import com.android.apksig.internal.apk.SignatureInfo; +import com.android.apksig.internal.util.Pair; +import com.android.apksig.util.DataSource; + +import java.io.IOException; +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; + +/** + * Source Stamp verifier. + * + * <p>V1 of the source stamp verifies the stamp signature of at most one signature scheme. + */ +public abstract class V1SourceStampVerifier { + + /** Hidden constructor to prevent instantiation. */ + private V1SourceStampVerifier() {} + + /** + * Verifies the provided APK's SourceStamp signatures and returns the result of verification. + * The APK must be considered verified only if {@link ApkSigningBlockUtils.Result#verified} is + * {@code true}. If verification fails, the result will contain errors -- see {@link + * ApkSigningBlockUtils.Result#getErrors()}. + * + * @throws NoSuchAlgorithmException if the APK's signatures cannot be verified because a + * required cryptographic algorithm implementation is missing + * @throws ApkSigningBlockUtils.SignatureNotFoundException if no SourceStamp signatures are + * found + * @throws IOException if an I/O error occurs when reading the APK + */ + public static ApkSigningBlockUtils.Result verify( + DataSource apk, + ApkUtils.ZipSections zipSections, + byte[] sourceStampCertificateDigest, + Map<ContentDigestAlgorithm, byte[]> apkContentDigests, + int minSdkVersion, + int maxSdkVersion) + throws IOException, NoSuchAlgorithmException, + ApkSigningBlockUtils.SignatureNotFoundException { + ApkSigningBlockUtils.Result result = + new ApkSigningBlockUtils.Result(ApkSigningBlockUtils.VERSION_SOURCE_STAMP); + SignatureInfo signatureInfo = + ApkSigningBlockUtils.findSignature( + apk, zipSections, V1_SOURCE_STAMP_BLOCK_ID, result); + + verify( + signatureInfo.signatureBlock, + sourceStampCertificateDigest, + apkContentDigests, + minSdkVersion, + maxSdkVersion, + result); + return result; + } + + /** + * Verifies the provided APK's SourceStamp signatures and outputs the results into the provided + * {@code result}. APK is considered verified only if there are no errors reported in the {@code + * result}. See {@link #verify(DataSource, ApkUtils.ZipSections, byte[], Map, int, int)} for + * more information about the contract of this method. + */ + private static void verify( + ByteBuffer sourceStampBlock, + byte[] sourceStampCertificateDigest, + Map<ContentDigestAlgorithm, byte[]> apkContentDigests, + int minSdkVersion, + int maxSdkVersion, + ApkSigningBlockUtils.Result result) + throws NoSuchAlgorithmException { + ApkSigningBlockUtils.Result.SignerInfo signerInfo = + new ApkSigningBlockUtils.Result.SignerInfo(); + result.signers.add(signerInfo); + try { + CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); + ByteBuffer sourceStampBlockData = + ApkSigningBlockUtils.getLengthPrefixedSlice(sourceStampBlock); + byte[] digestBytes = + encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes( + getApkDigests(apkContentDigests)); + SourceStampVerifier.verifyV1SourceStamp( + sourceStampBlockData, + certFactory, + signerInfo, + digestBytes, + sourceStampCertificateDigest, + minSdkVersion, + maxSdkVersion); + result.verified = !result.containsErrors() && !result.containsWarnings(); + } catch (CertificateException e) { + throw new IllegalStateException("Failed to obtain X.509 CertificateFactory", e); + } catch (ApkFormatException | BufferUnderflowException e) { + signerInfo.addWarning(ApkVerifier.Issue.SOURCE_STAMP_MALFORMED_SIGNATURE); + } + } + + private static List<Pair<Integer, byte[]>> getApkDigests( + Map<ContentDigestAlgorithm, byte[]> apkContentDigests) { + List<Pair<Integer, byte[]>> digests = new ArrayList<>(); + for (Map.Entry<ContentDigestAlgorithm, byte[]> apkContentDigest : + apkContentDigests.entrySet()) { + digests.add(Pair.of(apkContentDigest.getKey().getId(), apkContentDigest.getValue())); + } + Collections.sort(digests, Comparator.comparing(Pair::getFirst)); + return digests; + } +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/stamp/V2SourceStampSigner.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/stamp/V2SourceStampSigner.java new file mode 100644 index 0000000000..9283f02673 --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/stamp/V2SourceStampSigner.java @@ -0,0 +1,286 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.apk.stamp; + +import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2; +import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3; +import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_JAR_SIGNATURE_SCHEME; +import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsLengthPrefixedElement; +import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedElements; +import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes; + +import com.android.apksig.SigningCertificateLineage; +import com.android.apksig.internal.apk.ApkSigningBlockUtils; +import com.android.apksig.internal.apk.ApkSigningBlockUtils.SignerConfig; +import com.android.apksig.internal.apk.ContentDigestAlgorithm; +import com.android.apksig.internal.util.Pair; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.SignatureException; +import java.security.cert.CertificateEncodingException; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * SourceStamp signer. + * + * <p>SourceStamp improves traceability of apps with respect to unauthorized distribution. + * + * <p>The stamp is part of the APK that is protected by the signing block. + * + * <p>The APK contents hash is signed using the stamp key, and is saved as part of the signing + * block. + * + * <p>V2 of the source stamp allows signing the digests of more than one signature schemes. + */ +public class V2SourceStampSigner { + public static final int V2_SOURCE_STAMP_BLOCK_ID = + SourceStampConstants.V2_SOURCE_STAMP_BLOCK_ID; + + private final SignerConfig mSourceStampSignerConfig; + private final Map<Integer, Map<ContentDigestAlgorithm, byte[]>> mSignatureSchemeDigestInfos; + private final boolean mSourceStampTimestampEnabled; + + /** Hidden constructor to prevent instantiation. */ + private V2SourceStampSigner(Builder builder) { + mSourceStampSignerConfig = builder.mSourceStampSignerConfig; + mSignatureSchemeDigestInfos = builder.mSignatureSchemeDigestInfos; + mSourceStampTimestampEnabled = builder.mSourceStampTimestampEnabled; + } + + public static Pair<byte[], Integer> generateSourceStampBlock( + SignerConfig sourceStampSignerConfig, + Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeDigestInfos) + throws SignatureException, NoSuchAlgorithmException, InvalidKeyException { + return new Builder(sourceStampSignerConfig, + signatureSchemeDigestInfos).build().generateSourceStampBlock(); + } + + public Pair<byte[], Integer> generateSourceStampBlock() + throws SignatureException, NoSuchAlgorithmException, InvalidKeyException { + if (mSourceStampSignerConfig.certificates.isEmpty()) { + throw new SignatureException("No certificates configured for signer"); + } + + // Extract the digests for signature schemes. + List<Pair<Integer, byte[]>> signatureSchemeDigests = new ArrayList<>(); + getSignedDigestsFor( + VERSION_APK_SIGNATURE_SCHEME_V3, + mSignatureSchemeDigestInfos, + mSourceStampSignerConfig, + signatureSchemeDigests); + getSignedDigestsFor( + VERSION_APK_SIGNATURE_SCHEME_V2, + mSignatureSchemeDigestInfos, + mSourceStampSignerConfig, + signatureSchemeDigests); + getSignedDigestsFor( + VERSION_JAR_SIGNATURE_SCHEME, + mSignatureSchemeDigestInfos, + mSourceStampSignerConfig, + signatureSchemeDigests); + Collections.sort(signatureSchemeDigests, Comparator.comparing(Pair::getFirst)); + + SourceStampBlock sourceStampBlock = new SourceStampBlock(); + + try { + sourceStampBlock.stampCertificate = + mSourceStampSignerConfig.certificates.get(0).getEncoded(); + } catch (CertificateEncodingException e) { + throw new SignatureException( + "Retrieving the encoded form of the stamp certificate failed", e); + } + + sourceStampBlock.signedDigests = signatureSchemeDigests; + + sourceStampBlock.stampAttributes = encodeStampAttributes( + generateStampAttributes(mSourceStampSignerConfig.signingCertificateLineage)); + sourceStampBlock.signedStampAttributes = + ApkSigningBlockUtils.generateSignaturesOverData(mSourceStampSignerConfig, + sourceStampBlock.stampAttributes); + + // FORMAT: + // * length-prefixed bytes: X.509 certificate (ASN.1 DER encoded) + // * length-prefixed sequence of length-prefixed signed signature scheme digests: + // * uint32: signature scheme id + // * length-prefixed bytes: signed digests for the respective signature scheme + // * length-prefixed bytes: encoded stamp attributes + // * length-prefixed sequence of length-prefixed signed stamp attributes: + // * uint32: signature algorithm id + // * length-prefixed bytes: signed stamp attributes for the respective signature algorithm + byte[] sourceStampSignerBlock = + encodeAsSequenceOfLengthPrefixedElements( + new byte[][]{ + sourceStampBlock.stampCertificate, + encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes( + sourceStampBlock.signedDigests), + sourceStampBlock.stampAttributes, + encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes( + sourceStampBlock.signedStampAttributes), + }); + + // FORMAT: + // * length-prefixed stamp block. + return Pair.of(encodeAsLengthPrefixedElement(sourceStampSignerBlock), + SourceStampConstants.V2_SOURCE_STAMP_BLOCK_ID); + } + + private static void getSignedDigestsFor( + int signatureSchemeVersion, + Map<Integer, Map<ContentDigestAlgorithm, byte[]>> mSignatureSchemeDigestInfos, + SignerConfig mSourceStampSignerConfig, + List<Pair<Integer, byte[]>> signatureSchemeDigests) + throws NoSuchAlgorithmException, InvalidKeyException, SignatureException { + if (!mSignatureSchemeDigestInfos.containsKey(signatureSchemeVersion)) { + return; + } + + Map<ContentDigestAlgorithm, byte[]> digestInfo = + mSignatureSchemeDigestInfos.get(signatureSchemeVersion); + List<Pair<Integer, byte[]>> digests = new ArrayList<>(); + for (Map.Entry<ContentDigestAlgorithm, byte[]> digest : digestInfo.entrySet()) { + digests.add(Pair.of(digest.getKey().getId(), digest.getValue())); + } + Collections.sort(digests, Comparator.comparing(Pair::getFirst)); + + // FORMAT: + // * length-prefixed sequence of length-prefixed digests: + // * uint32: digest algorithm id + // * length-prefixed bytes: digest of the respective digest algorithm + byte[] digestBytes = + encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(digests); + + // FORMAT: + // * length-prefixed sequence of length-prefixed signed digests: + // * uint32: signature algorithm id + // * length-prefixed bytes: signed digest for the respective signature algorithm + List<Pair<Integer, byte[]>> signedDigest = + ApkSigningBlockUtils.generateSignaturesOverData( + mSourceStampSignerConfig, digestBytes); + + // FORMAT: + // * length-prefixed sequence of length-prefixed signed signature scheme digests: + // * uint32: signature scheme id + // * length-prefixed bytes: signed digests for the respective signature scheme + signatureSchemeDigests.add( + Pair.of( + signatureSchemeVersion, + encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes( + signedDigest))); + } + + private static byte[] encodeStampAttributes(Map<Integer, byte[]> stampAttributes) { + int payloadSize = 0; + for (byte[] attributeValue : stampAttributes.values()) { + // Pair size + Attribute ID + Attribute value + payloadSize += 4 + 4 + attributeValue.length; + } + + // FORMAT (little endian): + // * length-prefixed bytes: pair + // * uint32: ID + // * bytes: value + ByteBuffer result = ByteBuffer.allocate(4 + payloadSize); + result.order(ByteOrder.LITTLE_ENDIAN); + result.putInt(payloadSize); + for (Map.Entry<Integer, byte[]> stampAttribute : stampAttributes.entrySet()) { + // Pair size + result.putInt(4 + stampAttribute.getValue().length); + result.putInt(stampAttribute.getKey()); + result.put(stampAttribute.getValue()); + } + return result.array(); + } + + private Map<Integer, byte[]> generateStampAttributes(SigningCertificateLineage lineage) { + HashMap<Integer, byte[]> stampAttributes = new HashMap<>(); + + if (mSourceStampTimestampEnabled) { + // Write the current epoch time as the timestamp for the source stamp. + long timestamp = Instant.now().getEpochSecond(); + if (timestamp > 0) { + ByteBuffer attributeBuffer = ByteBuffer.allocate(8); + attributeBuffer.order(ByteOrder.LITTLE_ENDIAN); + attributeBuffer.putLong(timestamp); + stampAttributes.put(SourceStampConstants.STAMP_TIME_ATTR_ID, + attributeBuffer.array()); + } else { + // The epoch time should never be <= 0, and since security decisions can potentially + // be made based on the value in the timestamp, throw an Exception to ensure the + // issues with the environment are resolved before allowing the signing. + throw new IllegalStateException( + "Received an invalid value from Instant#getTimestamp: " + timestamp); + } + } + + if (lineage != null) { + stampAttributes.put(SourceStampConstants.PROOF_OF_ROTATION_ATTR_ID, + lineage.encodeSigningCertificateLineage()); + } + return stampAttributes; + } + + private static final class SourceStampBlock { + public byte[] stampCertificate; + public List<Pair<Integer, byte[]>> signedDigests; + // Optional stamp attributes that are not required for verification. + public byte[] stampAttributes; + public List<Pair<Integer, byte[]>> signedStampAttributes; + } + + /** Builder of {@link V2SourceStampSigner} instances. */ + public static class Builder { + private final SignerConfig mSourceStampSignerConfig; + private final Map<Integer, Map<ContentDigestAlgorithm, byte[]>> mSignatureSchemeDigestInfos; + private boolean mSourceStampTimestampEnabled = true; + + /** + * Instantiates a new {@code Builder} with the provided {@code sourceStampSignerConfig} + * and the {@code signatureSchemeDigestInfos}. + */ + public Builder(SignerConfig sourceStampSignerConfig, + Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeDigestInfos) { + mSourceStampSignerConfig = sourceStampSignerConfig; + mSignatureSchemeDigestInfos = signatureSchemeDigestInfos; + } + + /** + * Sets whether the source stamp should contain the timestamp attribute with the time + * at which the source stamp was signed. + */ + public Builder setSourceStampTimestampEnabled(boolean value) { + mSourceStampTimestampEnabled = value; + return this; + } + + /** + * Builds a new V2SourceStampSigner that can be used to generate a new source stamp + * block signed with the specified signing config. + */ + public V2SourceStampSigner build() { + return new V2SourceStampSigner(this); + } + } +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/stamp/V2SourceStampVerifier.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/stamp/V2SourceStampVerifier.java new file mode 100644 index 0000000000..a215b986a8 --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/stamp/V2SourceStampVerifier.java @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.apk.stamp; + +import static com.android.apksig.internal.apk.ApkSigningBlockUtilsLite.encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes; +import static com.android.apksig.internal.apk.stamp.SourceStampConstants.V2_SOURCE_STAMP_BLOCK_ID; + +import com.android.apksig.ApkVerificationIssue; +import com.android.apksig.Constants; +import com.android.apksig.apk.ApkFormatException; +import com.android.apksig.internal.apk.ApkSigResult; +import com.android.apksig.internal.apk.ApkSignerInfo; +import com.android.apksig.internal.apk.ApkSigningBlockUtilsLite; +import com.android.apksig.internal.apk.ContentDigestAlgorithm; +import com.android.apksig.internal.apk.SignatureInfo; +import com.android.apksig.internal.apk.SignatureNotFoundException; +import com.android.apksig.internal.util.Pair; +import com.android.apksig.util.DataSource; +import com.android.apksig.zip.ZipSections; + +import java.io.IOException; +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Source Stamp verifier. + * + * <p>V2 of the source stamp verifies the stamp signature of more than one signature schemes. + */ +public abstract class V2SourceStampVerifier { + + /** Hidden constructor to prevent instantiation. */ + private V2SourceStampVerifier() {} + + /** + * Verifies the provided APK's SourceStamp signatures and returns the result of verification. + * The APK must be considered verified only if {@link ApkSigResult#verified} is + * {@code true}. If verification fails, the result will contain errors -- see {@link + * ApkSigResult#getErrors()}. + * + * @throws NoSuchAlgorithmException if the APK's signatures cannot be verified because a + * required cryptographic algorithm implementation is missing + * @throws SignatureNotFoundException if no SourceStamp signatures are + * found + * @throws IOException if an I/O error occurs when reading the APK + */ + public static ApkSigResult verify( + DataSource apk, + ZipSections zipSections, + byte[] sourceStampCertificateDigest, + Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeApkContentDigests, + int minSdkVersion, + int maxSdkVersion) + throws IOException, NoSuchAlgorithmException, SignatureNotFoundException { + ApkSigResult result = + new ApkSigResult(Constants.VERSION_SOURCE_STAMP); + SignatureInfo signatureInfo = + ApkSigningBlockUtilsLite.findSignature( + apk, zipSections, V2_SOURCE_STAMP_BLOCK_ID); + + verify( + signatureInfo.signatureBlock, + sourceStampCertificateDigest, + signatureSchemeApkContentDigests, + minSdkVersion, + maxSdkVersion, + result); + return result; + } + + /** + * Verifies the provided APK's SourceStamp signatures and outputs the results into the provided + * {@code result}. APK is considered verified only if there are no errors reported in the {@code + * result}. See {@link #verify(DataSource, ZipSections, byte[], Map, int, int)} for + * more information about the contract of this method. + */ + private static void verify( + ByteBuffer sourceStampBlock, + byte[] sourceStampCertificateDigest, + Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeApkContentDigests, + int minSdkVersion, + int maxSdkVersion, + ApkSigResult result) + throws NoSuchAlgorithmException { + ApkSignerInfo signerInfo = new ApkSignerInfo(); + result.mSigners.add(signerInfo); + try { + CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); + ByteBuffer sourceStampBlockData = + ApkSigningBlockUtilsLite.getLengthPrefixedSlice(sourceStampBlock); + SourceStampVerifier.verifyV2SourceStamp( + sourceStampBlockData, + certFactory, + signerInfo, + getSignatureSchemeDigests(signatureSchemeApkContentDigests), + sourceStampCertificateDigest, + minSdkVersion, + maxSdkVersion); + result.verified = !result.containsErrors() && !result.containsWarnings(); + } catch (CertificateException e) { + throw new IllegalStateException("Failed to obtain X.509 CertificateFactory", e); + } catch (ApkFormatException | BufferUnderflowException e) { + signerInfo.addWarning(ApkVerificationIssue.SOURCE_STAMP_MALFORMED_SIGNATURE); + } + } + + private static Map<Integer, byte[]> getSignatureSchemeDigests( + Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeApkContentDigests) { + Map<Integer, byte[]> digests = new HashMap<>(); + for (Map.Entry<Integer, Map<ContentDigestAlgorithm, byte[]>> + signatureSchemeApkContentDigest : signatureSchemeApkContentDigests.entrySet()) { + List<Pair<Integer, byte[]>> apkDigests = + getApkDigests(signatureSchemeApkContentDigest.getValue()); + digests.put( + signatureSchemeApkContentDigest.getKey(), + encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(apkDigests)); + } + return digests; + } + + private static List<Pair<Integer, byte[]>> getApkDigests( + Map<ContentDigestAlgorithm, byte[]> apkContentDigests) { + List<Pair<Integer, byte[]>> digests = new ArrayList<>(); + for (Map.Entry<ContentDigestAlgorithm, byte[]> apkContentDigest : + apkContentDigests.entrySet()) { + digests.add(Pair.of(apkContentDigest.getKey().getId(), apkContentDigest.getValue())); + } + Collections.sort(digests, new Comparator<Pair<Integer, byte[]>>() { + @Override + public int compare(Pair<Integer, byte[]> pair1, Pair<Integer, byte[]> pair2) { + return pair1.getFirst() - pair2.getFirst(); + } + }); + return digests; + } +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v1/DigestAlgorithm.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v1/DigestAlgorithm.java new file mode 100644 index 0000000000..51b9810fa9 --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v1/DigestAlgorithm.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.apk.v1; + +import java.util.Comparator; + +/** + * Digest algorithm used with JAR signing (aka v1 signing scheme). + */ +public enum DigestAlgorithm { + /** SHA-1 */ + SHA1("SHA-1"), + + /** SHA2-256 */ + SHA256("SHA-256"); + + private final String mJcaMessageDigestAlgorithm; + + private DigestAlgorithm(String jcaMessageDigestAlgoritm) { + mJcaMessageDigestAlgorithm = jcaMessageDigestAlgoritm; + } + + /** + * Returns the {@link java.security.MessageDigest} algorithm represented by this digest + * algorithm. + */ + String getJcaMessageDigestAlgorithm() { + return mJcaMessageDigestAlgorithm; + } + + public static Comparator<DigestAlgorithm> BY_STRENGTH_COMPARATOR = new StrengthComparator(); + + private static class StrengthComparator implements Comparator<DigestAlgorithm> { + @Override + public int compare(DigestAlgorithm a1, DigestAlgorithm a2) { + switch (a1) { + case SHA1: + switch (a2) { + case SHA1: + return 0; + case SHA256: + return -1; + } + throw new RuntimeException("Unsupported algorithm: " + a2); + + case SHA256: + switch (a2) { + case SHA1: + return 1; + case SHA256: + return 0; + } + throw new RuntimeException("Unsupported algorithm: " + a2); + + default: + throw new RuntimeException("Unsupported algorithm: " + a1); + } + } + } +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeConstants.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeConstants.java new file mode 100644 index 0000000000..db1d15f618 --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeConstants.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.apk.v1; + +/** Constants used by the Jar Signing / V1 Signature Scheme signing and verification. */ +public class V1SchemeConstants { + private V1SchemeConstants() {} + + public static final String MANIFEST_ENTRY_NAME = "META-INF/MANIFEST.MF"; + public static final String SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME_STR = + "X-Android-APK-Signed"; +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeSigner.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeSigner.java new file mode 100644 index 0000000000..3cb109eff2 --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeSigner.java @@ -0,0 +1,586 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.apk.v1; + +import static com.android.apksig.Constants.MAX_APK_SIGNERS; +import static com.android.apksig.Constants.OID_RSA_ENCRYPTION; +import static com.android.apksig.internal.pkcs7.AlgorithmIdentifier.getSignerInfoDigestAlgorithmOid; +import static com.android.apksig.internal.pkcs7.AlgorithmIdentifier.getSignerInfoSignatureAlgorithm; + +import com.android.apksig.apk.ApkFormatException; +import com.android.apksig.internal.apk.ApkSigningBlockUtils; +import com.android.apksig.internal.asn1.Asn1EncodingException; +import com.android.apksig.internal.jar.ManifestWriter; +import com.android.apksig.internal.jar.SignatureFileWriter; +import com.android.apksig.internal.pkcs7.AlgorithmIdentifier; +import com.android.apksig.internal.util.Pair; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.security.InvalidKeyException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.Signature; +import java.security.SignatureException; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Base64; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.SortedMap; +import java.util.TreeMap; +import java.util.jar.Attributes; +import java.util.jar.Manifest; + +/** + * APK signer which uses JAR signing (aka v1 signing scheme). + * + * @see <a href="https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File">Signed JAR File</a> + */ +public abstract class V1SchemeSigner { + public static final String MANIFEST_ENTRY_NAME = V1SchemeConstants.MANIFEST_ENTRY_NAME; + + private static final Attributes.Name ATTRIBUTE_NAME_CREATED_BY = + new Attributes.Name("Created-By"); + private static final String ATTRIBUTE_VALUE_MANIFEST_VERSION = "1.0"; + private static final String ATTRIBUTE_VALUE_SIGNATURE_VERSION = "1.0"; + + private static final Attributes.Name SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME = + new Attributes.Name(V1SchemeConstants.SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME_STR); + + /** + * Signer configuration. + */ + public static class SignerConfig { + /** Name. */ + public String name; + + /** Private key. */ + public PrivateKey privateKey; + + /** + * Certificates, with the first certificate containing the public key corresponding to + * {@link #privateKey}. + */ + public List<X509Certificate> certificates; + + /** + * Digest algorithm used for the signature. + */ + public DigestAlgorithm signatureDigestAlgorithm; + + /** + * If DSA is the signing algorithm, whether or not deterministic DSA signing should be used. + */ + public boolean deterministicDsaSigning; + } + + /** Hidden constructor to prevent instantiation. */ + private V1SchemeSigner() {} + + /** + * Gets the JAR signing digest algorithm to be used for signing an APK using the provided key. + * + * @param minSdkVersion minimum API Level of the platform on which the APK may be installed (see + * AndroidManifest.xml minSdkVersion attribute) + * + * @throws InvalidKeyException if the provided key is not suitable for signing APKs using + * JAR signing (aka v1 signature scheme) + */ + public static DigestAlgorithm getSuggestedSignatureDigestAlgorithm( + PublicKey signingKey, int minSdkVersion) throws InvalidKeyException { + String keyAlgorithm = signingKey.getAlgorithm(); + if ("RSA".equalsIgnoreCase(keyAlgorithm) || OID_RSA_ENCRYPTION.equals((keyAlgorithm))) { + // Prior to API Level 18, only SHA-1 can be used with RSA. + if (minSdkVersion < 18) { + return DigestAlgorithm.SHA1; + } + return DigestAlgorithm.SHA256; + } else if ("DSA".equalsIgnoreCase(keyAlgorithm)) { + // Prior to API Level 21, only SHA-1 can be used with DSA + if (minSdkVersion < 21) { + return DigestAlgorithm.SHA1; + } else { + return DigestAlgorithm.SHA256; + } + } else if ("EC".equalsIgnoreCase(keyAlgorithm)) { + if (minSdkVersion < 18) { + throw new InvalidKeyException( + "ECDSA signatures only supported for minSdkVersion 18 and higher"); + } + return DigestAlgorithm.SHA256; + } else { + throw new InvalidKeyException("Unsupported key algorithm: " + keyAlgorithm); + } + } + + /** + * Returns a safe version of the provided signer name. + */ + public static String getSafeSignerName(String name) { + if (name.isEmpty()) { + throw new IllegalArgumentException("Empty name"); + } + + // According to https://docs.oracle.com/javase/tutorial/deployment/jar/signing.html, the + // name must not be longer than 8 characters and may contain only A-Z, 0-9, _, and -. + StringBuilder result = new StringBuilder(); + char[] nameCharsUpperCase = name.toUpperCase(Locale.US).toCharArray(); + for (int i = 0; i < Math.min(nameCharsUpperCase.length, 8); i++) { + char c = nameCharsUpperCase[i]; + if (((c >= 'A') && (c <= 'Z')) + || ((c >= '0') && (c <= '9')) + || (c == '-') + || (c == '_')) { + result.append(c); + } else { + result.append('_'); + } + } + return result.toString(); + } + + /** + * Returns a new {@link MessageDigest} instance corresponding to the provided digest algorithm. + */ + private static MessageDigest getMessageDigestInstance(DigestAlgorithm digestAlgorithm) + throws NoSuchAlgorithmException { + String jcaAlgorithm = digestAlgorithm.getJcaMessageDigestAlgorithm(); + return MessageDigest.getInstance(jcaAlgorithm); + } + + /** + * Returns the JCA {@link MessageDigest} algorithm corresponding to the provided digest + * algorithm. + */ + public static String getJcaMessageDigestAlgorithm(DigestAlgorithm digestAlgorithm) { + return digestAlgorithm.getJcaMessageDigestAlgorithm(); + } + + /** + * Returns {@code true} if the provided JAR entry must be mentioned in signed JAR archive's + * manifest. + */ + public static boolean isJarEntryDigestNeededInManifest(String entryName) { + // See https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File + + // Entries which represent directories sould not be listed in the manifest. + if (entryName.endsWith("/")) { + return false; + } + + // Entries outside of META-INF must be listed in the manifest. + if (!entryName.startsWith("META-INF/")) { + return true; + } + // Entries in subdirectories of META-INF must be listed in the manifest. + if (entryName.indexOf('/', "META-INF/".length()) != -1) { + return true; + } + + // Ignored file names (case-insensitive) in META-INF directory: + // MANIFEST.MF + // *.SF + // *.RSA + // *.DSA + // *.EC + // SIG-* + String fileNameLowerCase = + entryName.substring("META-INF/".length()).toLowerCase(Locale.US); + if (("manifest.mf".equals(fileNameLowerCase)) + || (fileNameLowerCase.endsWith(".sf")) + || (fileNameLowerCase.endsWith(".rsa")) + || (fileNameLowerCase.endsWith(".dsa")) + || (fileNameLowerCase.endsWith(".ec")) + || (fileNameLowerCase.startsWith("sig-"))) { + return false; + } + return true; + } + + /** + * Signs the provided APK using JAR signing (aka v1 signature scheme) and returns the list of + * JAR entries which need to be added to the APK as part of the signature. + * + * @param signerConfigs signer configurations, one for each signer. At least one signer config + * must be provided. + * + * @throws ApkFormatException if the source manifest is malformed + * @throws NoSuchAlgorithmException if a required cryptographic algorithm implementation is + * missing + * @throws InvalidKeyException if a signing key is not suitable for this signature scheme or + * cannot be used in general + * @throws SignatureException if an error occurs when computing digests of generating + * signatures + */ + public static List<Pair<String, byte[]>> sign( + List<SignerConfig> signerConfigs, + DigestAlgorithm jarEntryDigestAlgorithm, + Map<String, byte[]> jarEntryDigests, + List<Integer> apkSigningSchemeIds, + byte[] sourceManifestBytes, + String createdBy) + throws NoSuchAlgorithmException, ApkFormatException, InvalidKeyException, + CertificateException, SignatureException { + if (signerConfigs.isEmpty()) { + throw new IllegalArgumentException("At least one signer config must be provided"); + } + if (signerConfigs.size() > MAX_APK_SIGNERS) { + throw new IllegalArgumentException( + "APK Signature Scheme v1 only supports a maximum of " + MAX_APK_SIGNERS + ", " + + signerConfigs.size() + " provided"); + } + OutputManifestFile manifest = + generateManifestFile( + jarEntryDigestAlgorithm, jarEntryDigests, sourceManifestBytes); + + return signManifest( + signerConfigs, jarEntryDigestAlgorithm, apkSigningSchemeIds, createdBy, manifest); + } + + /** + * Signs the provided APK using JAR signing (aka v1 signature scheme) and returns the list of + * JAR entries which need to be added to the APK as part of the signature. + * + * @param signerConfigs signer configurations, one for each signer. At least one signer config + * must be provided. + * + * @throws InvalidKeyException if a signing key is not suitable for this signature scheme or + * cannot be used in general + * @throws SignatureException if an error occurs when computing digests of generating + * signatures + */ + public static List<Pair<String, byte[]>> signManifest( + List<SignerConfig> signerConfigs, + DigestAlgorithm digestAlgorithm, + List<Integer> apkSigningSchemeIds, + String createdBy, + OutputManifestFile manifest) + throws NoSuchAlgorithmException, InvalidKeyException, CertificateException, + SignatureException { + if (signerConfigs.isEmpty()) { + throw new IllegalArgumentException("At least one signer config must be provided"); + } + + // For each signer output .SF and .(RSA|DSA|EC) file, then output MANIFEST.MF. + List<Pair<String, byte[]>> signatureJarEntries = + new ArrayList<>(2 * signerConfigs.size() + 1); + byte[] sfBytes = + generateSignatureFile(apkSigningSchemeIds, digestAlgorithm, createdBy, manifest); + for (SignerConfig signerConfig : signerConfigs) { + String signerName = signerConfig.name; + byte[] signatureBlock; + try { + signatureBlock = generateSignatureBlock(signerConfig, sfBytes); + } catch (InvalidKeyException e) { + throw new InvalidKeyException( + "Failed to sign using signer \"" + signerName + "\"", e); + } catch (CertificateException e) { + throw new CertificateException( + "Failed to sign using signer \"" + signerName + "\"", e); + } catch (SignatureException e) { + throw new SignatureException( + "Failed to sign using signer \"" + signerName + "\"", e); + } + signatureJarEntries.add(Pair.of("META-INF/" + signerName + ".SF", sfBytes)); + PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey(); + String signatureBlockFileName = + "META-INF/" + signerName + "." + + publicKey.getAlgorithm().toUpperCase(Locale.US); + signatureJarEntries.add( + Pair.of(signatureBlockFileName, signatureBlock)); + } + signatureJarEntries.add(Pair.of(V1SchemeConstants.MANIFEST_ENTRY_NAME, manifest.contents)); + return signatureJarEntries; + } + + /** + * Returns the names of JAR entries which this signer will produce as part of v1 signature. + */ + public static Set<String> getOutputEntryNames(List<SignerConfig> signerConfigs) { + Set<String> result = new HashSet<>(2 * signerConfigs.size() + 1); + for (SignerConfig signerConfig : signerConfigs) { + String signerName = signerConfig.name; + result.add("META-INF/" + signerName + ".SF"); + PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey(); + String signatureBlockFileName = + "META-INF/" + signerName + "." + + publicKey.getAlgorithm().toUpperCase(Locale.US); + result.add(signatureBlockFileName); + } + result.add(V1SchemeConstants.MANIFEST_ENTRY_NAME); + return result; + } + + /** + * Generated and returns the {@code META-INF/MANIFEST.MF} file based on the provided (optional) + * input {@code MANIFEST.MF} and digests of JAR entries covered by the manifest. + */ + public static OutputManifestFile generateManifestFile( + DigestAlgorithm jarEntryDigestAlgorithm, + Map<String, byte[]> jarEntryDigests, + byte[] sourceManifestBytes) throws ApkFormatException { + Manifest sourceManifest = null; + if (sourceManifestBytes != null) { + try { + sourceManifest = new Manifest(new ByteArrayInputStream(sourceManifestBytes)); + } catch (IOException e) { + throw new ApkFormatException("Malformed source META-INF/MANIFEST.MF", e); + } + } + ByteArrayOutputStream manifestOut = new ByteArrayOutputStream(); + Attributes mainAttrs = new Attributes(); + // Copy the main section from the source manifest (if provided). Otherwise use defaults. + // NOTE: We don't output our own Created-By header because this signer did not create the + // JAR/APK being signed -- the signer only adds signatures to the already existing + // JAR/APK. + if (sourceManifest != null) { + mainAttrs.putAll(sourceManifest.getMainAttributes()); + } else { + mainAttrs.put(Attributes.Name.MANIFEST_VERSION, ATTRIBUTE_VALUE_MANIFEST_VERSION); + } + + try { + ManifestWriter.writeMainSection(manifestOut, mainAttrs); + } catch (IOException e) { + throw new RuntimeException("Failed to write in-memory MANIFEST.MF", e); + } + + List<String> sortedEntryNames = new ArrayList<>(jarEntryDigests.keySet()); + Collections.sort(sortedEntryNames); + SortedMap<String, byte[]> invidualSectionsContents = new TreeMap<>(); + String entryDigestAttributeName = getEntryDigestAttributeName(jarEntryDigestAlgorithm); + for (String entryName : sortedEntryNames) { + checkEntryNameValid(entryName); + byte[] entryDigest = jarEntryDigests.get(entryName); + Attributes entryAttrs = new Attributes(); + entryAttrs.putValue( + entryDigestAttributeName, + Base64.getEncoder().encodeToString(entryDigest)); + ByteArrayOutputStream sectionOut = new ByteArrayOutputStream(); + byte[] sectionBytes; + try { + ManifestWriter.writeIndividualSection(sectionOut, entryName, entryAttrs); + sectionBytes = sectionOut.toByteArray(); + manifestOut.write(sectionBytes); + } catch (IOException e) { + throw new RuntimeException("Failed to write in-memory MANIFEST.MF", e); + } + invidualSectionsContents.put(entryName, sectionBytes); + } + + OutputManifestFile result = new OutputManifestFile(); + result.contents = manifestOut.toByteArray(); + result.mainSectionAttributes = mainAttrs; + result.individualSectionsContents = invidualSectionsContents; + return result; + } + + private static void checkEntryNameValid(String name) throws ApkFormatException { + // JAR signing spec says CR, LF, and NUL are not permitted in entry names + // CR or LF in entry names will result in malformed MANIFEST.MF and .SF files because there + // is no way to escape characters in MANIFEST.MF and .SF files. NUL can, presumably, cause + // issues when parsing using C and C++ like languages. + for (char c : name.toCharArray()) { + if ((c == '\r') || (c == '\n') || (c == 0)) { + throw new ApkFormatException( + String.format( + "Unsupported character 0x%1$02x in ZIP entry name \"%2$s\"", + (int) c, + name)); + } + } + } + + public static class OutputManifestFile { + public byte[] contents; + public SortedMap<String, byte[]> individualSectionsContents; + public Attributes mainSectionAttributes; + } + + private static byte[] generateSignatureFile( + List<Integer> apkSignatureSchemeIds, + DigestAlgorithm manifestDigestAlgorithm, + String createdBy, + OutputManifestFile manifest) throws NoSuchAlgorithmException { + Manifest sf = new Manifest(); + Attributes mainAttrs = sf.getMainAttributes(); + mainAttrs.put(Attributes.Name.SIGNATURE_VERSION, ATTRIBUTE_VALUE_SIGNATURE_VERSION); + mainAttrs.put(ATTRIBUTE_NAME_CREATED_BY, createdBy); + if (!apkSignatureSchemeIds.isEmpty()) { + // Add APK Signature Scheme v2 (and newer) signature stripping protection. + // This attribute indicates that this APK is supposed to have been signed using one or + // more APK-specific signature schemes in addition to the standard JAR signature scheme + // used by this code. APK signature verifier should reject the APK if it does not + // contain a signature for the signature scheme the verifier prefers out of this set. + StringBuilder attrValue = new StringBuilder(); + for (int id : apkSignatureSchemeIds) { + if (attrValue.length() > 0) { + attrValue.append(", "); + } + attrValue.append(String.valueOf(id)); + } + mainAttrs.put( + SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME, + attrValue.toString()); + } + + // Add main attribute containing the digest of MANIFEST.MF. + MessageDigest md = getMessageDigestInstance(manifestDigestAlgorithm); + mainAttrs.putValue( + getManifestDigestAttributeName(manifestDigestAlgorithm), + Base64.getEncoder().encodeToString(md.digest(manifest.contents))); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + try { + SignatureFileWriter.writeMainSection(out, mainAttrs); + } catch (IOException e) { + throw new RuntimeException("Failed to write in-memory .SF file", e); + } + String entryDigestAttributeName = getEntryDigestAttributeName(manifestDigestAlgorithm); + for (Map.Entry<String, byte[]> manifestSection + : manifest.individualSectionsContents.entrySet()) { + String sectionName = manifestSection.getKey(); + byte[] sectionContents = manifestSection.getValue(); + byte[] sectionDigest = md.digest(sectionContents); + Attributes attrs = new Attributes(); + attrs.putValue( + entryDigestAttributeName, + Base64.getEncoder().encodeToString(sectionDigest)); + + try { + SignatureFileWriter.writeIndividualSection(out, sectionName, attrs); + } catch (IOException e) { + throw new RuntimeException("Failed to write in-memory .SF file", e); + } + } + + // A bug in the java.util.jar implementation of Android platforms up to version 1.6 will + // cause a spurious IOException to be thrown if the length of the signature file is a + // multiple of 1024 bytes. As a workaround, add an extra CRLF in this case. + if ((out.size() > 0) && ((out.size() % 1024) == 0)) { + try { + SignatureFileWriter.writeSectionDelimiter(out); + } catch (IOException e) { + throw new RuntimeException("Failed to write to ByteArrayOutputStream", e); + } + } + + return out.toByteArray(); + } + + + + /** + * Generates the CMS PKCS #7 signature block corresponding to the provided signature file and + * signing configuration. + */ + private static byte[] generateSignatureBlock( + SignerConfig signerConfig, byte[] signatureFileBytes) + throws NoSuchAlgorithmException, InvalidKeyException, CertificateException, + SignatureException { + // Obtain relevant bits of signing configuration + List<X509Certificate> signerCerts = signerConfig.certificates; + X509Certificate signingCert = signerCerts.get(0); + PublicKey publicKey = signingCert.getPublicKey(); + DigestAlgorithm digestAlgorithm = signerConfig.signatureDigestAlgorithm; + Pair<String, AlgorithmIdentifier> signatureAlgs = + getSignerInfoSignatureAlgorithm(publicKey, digestAlgorithm, + signerConfig.deterministicDsaSigning); + String jcaSignatureAlgorithm = signatureAlgs.getFirst(); + + // Generate the cryptographic signature of the signature file + byte[] signatureBytes; + try { + Signature signature = Signature.getInstance(jcaSignatureAlgorithm); + signature.initSign(signerConfig.privateKey); + signature.update(signatureFileBytes); + signatureBytes = signature.sign(); + } catch (InvalidKeyException e) { + throw new InvalidKeyException("Failed to sign using " + jcaSignatureAlgorithm, e); + } catch (SignatureException e) { + throw new SignatureException("Failed to sign using " + jcaSignatureAlgorithm, e); + } + + // Verify the signature against the public key in the signing certificate + try { + Signature signature = Signature.getInstance(jcaSignatureAlgorithm); + signature.initVerify(publicKey); + signature.update(signatureFileBytes); + if (!signature.verify(signatureBytes)) { + throw new SignatureException("Signature did not verify"); + } + } catch (InvalidKeyException e) { + throw new InvalidKeyException( + "Failed to verify generated " + jcaSignatureAlgorithm + " signature using" + + " public key from certificate", + e); + } catch (SignatureException e) { + throw new SignatureException( + "Failed to verify generated " + jcaSignatureAlgorithm + " signature using" + + " public key from certificate", + e); + } + + AlgorithmIdentifier digestAlgorithmId = + getSignerInfoDigestAlgorithmOid(digestAlgorithm); + AlgorithmIdentifier signatureAlgorithmId = signatureAlgs.getSecond(); + try { + return ApkSigningBlockUtils.generatePkcs7DerEncodedMessage( + signatureBytes, + null, + signerCerts, digestAlgorithmId, + signatureAlgorithmId); + } catch (Asn1EncodingException | CertificateEncodingException ex) { + throw new SignatureException("Failed to encode signature block"); + } + } + + + + private static String getEntryDigestAttributeName(DigestAlgorithm digestAlgorithm) { + switch (digestAlgorithm) { + case SHA1: + return "SHA1-Digest"; + case SHA256: + return "SHA-256-Digest"; + default: + throw new IllegalArgumentException( + "Unexpected content digest algorithm: " + digestAlgorithm); + } + } + + private static String getManifestDigestAttributeName(DigestAlgorithm digestAlgorithm) { + switch (digestAlgorithm) { + case SHA1: + return "SHA1-Digest-Manifest"; + case SHA256: + return "SHA-256-Digest-Manifest"; + default: + throw new IllegalArgumentException( + "Unexpected content digest algorithm: " + digestAlgorithm); + } + } +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeVerifier.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeVerifier.java new file mode 100644 index 0000000000..f3fd64156f --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeVerifier.java @@ -0,0 +1,1570 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.apk.v1; + +import static com.android.apksig.Constants.MAX_APK_SIGNERS; +import static com.android.apksig.internal.oid.OidConstants.getSigAlgSupportedApiLevels; +import static com.android.apksig.internal.pkcs7.AlgorithmIdentifier.getJcaDigestAlgorithm; +import static com.android.apksig.internal.pkcs7.AlgorithmIdentifier.getJcaSignatureAlgorithm; +import static com.android.apksig.internal.x509.Certificate.findCertificate; +import static com.android.apksig.internal.x509.Certificate.parseCertificates; + +import com.android.apksig.ApkVerifier.Issue; +import com.android.apksig.ApkVerifier.IssueWithParams; +import com.android.apksig.apk.ApkFormatException; +import com.android.apksig.apk.ApkUtils; +import com.android.apksig.internal.apk.ApkSigningBlockUtils; +import com.android.apksig.internal.asn1.Asn1BerParser; +import com.android.apksig.internal.asn1.Asn1Class; +import com.android.apksig.internal.asn1.Asn1DecodingException; +import com.android.apksig.internal.asn1.Asn1Field; +import com.android.apksig.internal.asn1.Asn1OpaqueObject; +import com.android.apksig.internal.asn1.Asn1Type; +import com.android.apksig.internal.jar.ManifestParser; +import com.android.apksig.internal.oid.OidConstants; +import com.android.apksig.internal.pkcs7.Attribute; +import com.android.apksig.internal.pkcs7.ContentInfo; +import com.android.apksig.internal.pkcs7.Pkcs7Constants; +import com.android.apksig.internal.pkcs7.Pkcs7DecodingException; +import com.android.apksig.internal.pkcs7.SignedData; +import com.android.apksig.internal.pkcs7.SignerInfo; +import com.android.apksig.internal.util.AndroidSdkVersion; +import com.android.apksig.internal.util.ByteBufferUtils; +import com.android.apksig.internal.util.InclusiveIntRange; +import com.android.apksig.internal.util.Pair; +import com.android.apksig.internal.zip.CentralDirectoryRecord; +import com.android.apksig.internal.zip.LocalFileRecord; +import com.android.apksig.internal.zip.ZipUtils; +import com.android.apksig.util.DataSinks; +import com.android.apksig.util.DataSource; +import com.android.apksig.zip.ZipFormatException; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.security.InvalidKeyException; +import java.security.KeyFactory; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.Principal; +import java.security.PublicKey; +import java.security.Signature; +import java.security.SignatureException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.X509EncodedKeySpec; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Base64; +import java.util.Base64.Decoder; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.StringTokenizer; +import java.util.jar.Attributes; + +/** + * APK verifier which uses JAR signing (aka v1 signing scheme). + * + * @see <a href="https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File">Signed JAR File</a> + */ +public abstract class V1SchemeVerifier { + private V1SchemeVerifier() {} + + /** + * Verifies the provided APK's JAR signatures and returns the result of verification. APK is + * considered verified only if {@link Result#verified} is {@code true}. If verification fails, + * the result will contain errors -- see {@link Result#getErrors()}. + * + * <p>Verification succeeds iff the APK's JAR signatures are expected to verify on all Android + * platform versions in the {@code [minSdkVersion, maxSdkVersion]} range. If the APK's signature + * is expected to not verify on any of the specified platform versions, this method returns a + * result with one or more errors and whose {@code Result.verified == false}, or this method + * throws an exception. + * + * @throws ApkFormatException if the APK is malformed + * @throws IOException if an I/O error occurs when reading the APK + * @throws NoSuchAlgorithmException if the APK's JAR signatures cannot be verified because a + * required cryptographic algorithm implementation is missing + */ + public static Result verify( + DataSource apk, + ApkUtils.ZipSections apkSections, + Map<Integer, String> supportedApkSigSchemeNames, + Set<Integer> foundApkSigSchemeIds, + int minSdkVersion, + int maxSdkVersion) throws IOException, ApkFormatException, NoSuchAlgorithmException { + if (minSdkVersion > maxSdkVersion) { + throw new IllegalArgumentException( + "minSdkVersion (" + minSdkVersion + ") > maxSdkVersion (" + maxSdkVersion + + ")"); + } + + Result result = new Result(); + + // Parse the ZIP Central Directory and check that there are no entries with duplicate names. + List<CentralDirectoryRecord> cdRecords = parseZipCentralDirectory(apk, apkSections); + Set<String> cdEntryNames = checkForDuplicateEntries(cdRecords, result); + if (result.containsErrors()) { + return result; + } + + // Verify JAR signature(s). + Signers.verify( + apk, + apkSections.getZipCentralDirectoryOffset(), + cdRecords, + cdEntryNames, + supportedApkSigSchemeNames, + foundApkSigSchemeIds, + minSdkVersion, + maxSdkVersion, + result); + + return result; + } + + /** + * Returns the set of entry names and reports any duplicate entry names in the {@code result} + * as errors. + */ + private static Set<String> checkForDuplicateEntries( + List<CentralDirectoryRecord> cdRecords, Result result) { + Set<String> cdEntryNames = new HashSet<>(cdRecords.size()); + Set<String> duplicateCdEntryNames = null; + for (CentralDirectoryRecord cdRecord : cdRecords) { + String entryName = cdRecord.getName(); + if (!cdEntryNames.add(entryName)) { + // This is an error. Report this once per duplicate name. + if (duplicateCdEntryNames == null) { + duplicateCdEntryNames = new HashSet<>(); + } + if (duplicateCdEntryNames.add(entryName)) { + result.addError(Issue.JAR_SIG_DUPLICATE_ZIP_ENTRY, entryName); + } + } + } + return cdEntryNames; + } + + /** + * Parses raw representation of MANIFEST.MF file into a pair of main entry manifest section + * representation and a mapping between entry name and its manifest section representation. + * + * @param manifestBytes raw representation of Manifest.MF + * @param cdEntryNames expected set of entry names + * @param result object to keep track of errors that happened during the parsing + * @return a pair of main entry manifest section representation and a mapping between entry name + * and its manifest section representation + */ + public static Pair<ManifestParser.Section, Map<String, ManifestParser.Section>> parseManifest( + byte[] manifestBytes, Set<String> cdEntryNames, Result result) { + ManifestParser manifest = new ManifestParser(manifestBytes); + ManifestParser.Section manifestMainSection = manifest.readSection(); + List<ManifestParser.Section> manifestIndividualSections = manifest.readAllSections(); + Map<String, ManifestParser.Section> entryNameToManifestSection = + new HashMap<>(manifestIndividualSections.size()); + int manifestSectionNumber = 0; + for (ManifestParser.Section manifestSection : manifestIndividualSections) { + manifestSectionNumber++; + String entryName = manifestSection.getName(); + if (entryName == null) { + result.addError(Issue.JAR_SIG_UNNNAMED_MANIFEST_SECTION, manifestSectionNumber); + continue; + } + if (entryNameToManifestSection.put(entryName, manifestSection) != null) { + result.addError(Issue.JAR_SIG_DUPLICATE_MANIFEST_SECTION, entryName); + continue; + } + if (!cdEntryNames.contains(entryName)) { + result.addError( + Issue.JAR_SIG_MISSING_ZIP_ENTRY_REFERENCED_IN_MANIFEST, entryName); + continue; + } + } + return Pair.of(manifestMainSection, entryNameToManifestSection); + } + + /** + * All JAR signers of an APK. + */ + private static class Signers { + + /** + * Verifies JAR signatures of the provided APK and populates the provided result container + * with errors, warnings, and information about signers. The APK is considered verified if + * the {@link Result#verified} is {@code true}. + */ + private static void verify( + DataSource apk, + long cdStartOffset, + List<CentralDirectoryRecord> cdRecords, + Set<String> cdEntryNames, + Map<Integer, String> supportedApkSigSchemeNames, + Set<Integer> foundApkSigSchemeIds, + int minSdkVersion, + int maxSdkVersion, + Result result) throws ApkFormatException, IOException, NoSuchAlgorithmException { + + // Find JAR manifest and signature block files. + CentralDirectoryRecord manifestEntry = null; + Map<String, CentralDirectoryRecord> sigFileEntries = new HashMap<>(1); + List<CentralDirectoryRecord> sigBlockEntries = new ArrayList<>(1); + for (CentralDirectoryRecord cdRecord : cdRecords) { + String entryName = cdRecord.getName(); + if (!entryName.startsWith("META-INF/")) { + continue; + } + if ((manifestEntry == null) && (V1SchemeConstants.MANIFEST_ENTRY_NAME.equals( + entryName))) { + manifestEntry = cdRecord; + continue; + } + if (entryName.endsWith(".SF")) { + sigFileEntries.put(entryName, cdRecord); + continue; + } + if ((entryName.endsWith(".RSA")) + || (entryName.endsWith(".DSA")) + || (entryName.endsWith(".EC"))) { + sigBlockEntries.add(cdRecord); + continue; + } + } + if (manifestEntry == null) { + result.addError(Issue.JAR_SIG_NO_MANIFEST); + return; + } + + // Parse the JAR manifest and check that all JAR entries it references exist in the APK. + byte[] manifestBytes; + try { + manifestBytes = + LocalFileRecord.getUncompressedData(apk, manifestEntry, cdStartOffset); + } catch (ZipFormatException e) { + throw new ApkFormatException("Malformed ZIP entry: " + manifestEntry.getName(), e); + } + + Pair<ManifestParser.Section, Map<String, ManifestParser.Section>> manifestSections = + parseManifest(manifestBytes, cdEntryNames, result); + + if (result.containsErrors()) { + return; + } + + ManifestParser.Section manifestMainSection = manifestSections.getFirst(); + Map<String, ManifestParser.Section> entryNameToManifestSection = + manifestSections.getSecond(); + + // STATE OF AFFAIRS: + // * All JAR entries listed in JAR manifest are present in the APK. + + // Identify signers + List<Signer> signers = new ArrayList<>(sigBlockEntries.size()); + for (CentralDirectoryRecord sigBlockEntry : sigBlockEntries) { + String sigBlockEntryName = sigBlockEntry.getName(); + int extensionDelimiterIndex = sigBlockEntryName.lastIndexOf('.'); + if (extensionDelimiterIndex == -1) { + throw new RuntimeException( + "Signature block file name does not contain extension: " + + sigBlockEntryName); + } + String sigFileEntryName = + sigBlockEntryName.substring(0, extensionDelimiterIndex) + ".SF"; + CentralDirectoryRecord sigFileEntry = sigFileEntries.get(sigFileEntryName); + if (sigFileEntry == null) { + result.addWarning( + Issue.JAR_SIG_MISSING_FILE, sigBlockEntryName, sigFileEntryName); + continue; + } + String signerName = sigBlockEntryName.substring("META-INF/".length()); + Result.SignerInfo signerInfo = + new Result.SignerInfo( + signerName, sigBlockEntryName, sigFileEntry.getName()); + Signer signer = new Signer(signerName, sigBlockEntry, sigFileEntry, signerInfo); + signers.add(signer); + } + if (signers.isEmpty()) { + result.addError(Issue.JAR_SIG_NO_SIGNATURES); + return; + } + if (signers.size() > MAX_APK_SIGNERS) { + result.addError(Issue.JAR_SIG_MAX_SIGNATURES_EXCEEDED, MAX_APK_SIGNERS, + signers.size()); + return; + } + + // Verify each signer's signature block file .(RSA|DSA|EC) against the corresponding + // signature file .SF. Any error encountered for any signer terminates verification, to + // mimic Android's behavior. + for (Signer signer : signers) { + signer.verifySigBlockAgainstSigFile( + apk, cdStartOffset, minSdkVersion, maxSdkVersion); + if (signer.getResult().containsErrors()) { + result.signers.add(signer.getResult()); + } + } + if (result.containsErrors()) { + return; + } + // STATE OF AFFAIRS: + // * All JAR entries listed in JAR manifest are present in the APK. + // * All signature files (.SF) verify against corresponding block files (.RSA|.DSA|.EC). + + // Verify each signer's signature file (.SF) against the JAR manifest. + List<Signer> remainingSigners = new ArrayList<>(signers.size()); + for (Signer signer : signers) { + signer.verifySigFileAgainstManifest( + manifestBytes, + manifestMainSection, + entryNameToManifestSection, + supportedApkSigSchemeNames, + foundApkSigSchemeIds, + minSdkVersion, + maxSdkVersion); + if (signer.isIgnored()) { + result.ignoredSigners.add(signer.getResult()); + } else { + if (signer.getResult().containsErrors()) { + result.signers.add(signer.getResult()); + } else { + remainingSigners.add(signer); + } + } + } + if (result.containsErrors()) { + return; + } + signers = remainingSigners; + if (signers.isEmpty()) { + result.addError(Issue.JAR_SIG_NO_SIGNATURES); + return; + } + // STATE OF AFFAIRS: + // * All signature files (.SF) verify against corresponding block files (.RSA|.DSA|.EC). + // * Contents of all JAR manifest sections listed in .SF files verify against .SF files. + // * All JAR entries listed in JAR manifest are present in the APK. + + // Verify data of JAR entries against JAR manifest and .SF files. On Android, an APK's + // JAR entry is considered signed by signers associated with an .SF file iff the entry + // is mentioned in the .SF file and the entry's digest(s) mentioned in the JAR manifest + // match theentry's uncompressed data. Android requires that all such JAR entries are + // signed by the same set of signers. This set may be smaller than the set of signers + // we've identified so far. + Set<Signer> apkSigners = + verifyJarEntriesAgainstManifestAndSigners( + apk, + cdStartOffset, + cdRecords, + entryNameToManifestSection, + signers, + minSdkVersion, + maxSdkVersion, + result); + if (result.containsErrors()) { + return; + } + // STATE OF AFFAIRS: + // * All signature files (.SF) verify against corresponding block files (.RSA|.DSA|.EC). + // * Contents of all JAR manifest sections listed in .SF files verify against .SF files. + // * All JAR entries listed in JAR manifest are present in the APK. + // * All JAR entries present in the APK and supposed to be covered by JAR signature + // (i.e., reside outside of META-INF/) are covered by signatures from the same set + // of signers. + + // Report any JAR entries which aren't covered by signature. + Set<String> signatureEntryNames = new HashSet<>(1 + result.signers.size() * 2); + signatureEntryNames.add(manifestEntry.getName()); + for (Signer signer : apkSigners) { + signatureEntryNames.add(signer.getSignatureBlockEntryName()); + signatureEntryNames.add(signer.getSignatureFileEntryName()); + } + for (CentralDirectoryRecord cdRecord : cdRecords) { + String entryName = cdRecord.getName(); + if ((entryName.startsWith("META-INF/")) + && (!entryName.endsWith("/")) + && (!signatureEntryNames.contains(entryName))) { + result.addWarning(Issue.JAR_SIG_UNPROTECTED_ZIP_ENTRY, entryName); + } + } + + // Reflect the sets of used signers and ignored signers in the result. + for (Signer signer : signers) { + if (apkSigners.contains(signer)) { + result.signers.add(signer.getResult()); + } else { + result.ignoredSigners.add(signer.getResult()); + } + } + + result.verified = true; + } + } + + static class Signer { + private final String mName; + private final Result.SignerInfo mResult; + private final CentralDirectoryRecord mSignatureFileEntry; + private final CentralDirectoryRecord mSignatureBlockEntry; + private boolean mIgnored; + + private byte[] mSigFileBytes; + private Set<String> mSigFileEntryNames; + + private Signer( + String name, + CentralDirectoryRecord sigBlockEntry, + CentralDirectoryRecord sigFileEntry, + Result.SignerInfo result) { + mName = name; + mResult = result; + mSignatureBlockEntry = sigBlockEntry; + mSignatureFileEntry = sigFileEntry; + } + + public String getName() { + return mName; + } + + public String getSignatureFileEntryName() { + return mSignatureFileEntry.getName(); + } + + public String getSignatureBlockEntryName() { + return mSignatureBlockEntry.getName(); + } + + void setIgnored() { + mIgnored = true; + } + + public boolean isIgnored() { + return mIgnored; + } + + public Set<String> getSigFileEntryNames() { + return mSigFileEntryNames; + } + + public Result.SignerInfo getResult() { + return mResult; + } + + public void verifySigBlockAgainstSigFile( + DataSource apk, long cdStartOffset, int minSdkVersion, int maxSdkVersion) + throws IOException, ApkFormatException, NoSuchAlgorithmException { + // Obtain the signature block from the APK + byte[] sigBlockBytes; + try { + sigBlockBytes = + LocalFileRecord.getUncompressedData( + apk, mSignatureBlockEntry, cdStartOffset); + } catch (ZipFormatException e) { + throw new ApkFormatException( + "Malformed ZIP entry: " + mSignatureBlockEntry.getName(), e); + } + // Obtain the signature file from the APK + try { + mSigFileBytes = + LocalFileRecord.getUncompressedData( + apk, mSignatureFileEntry, cdStartOffset); + } catch (ZipFormatException e) { + throw new ApkFormatException( + "Malformed ZIP entry: " + mSignatureFileEntry.getName(), e); + } + + // Extract PKCS #7 SignedData from the signature block + SignedData signedData; + try { + ContentInfo contentInfo = + Asn1BerParser.parse(ByteBuffer.wrap(sigBlockBytes), ContentInfo.class); + if (!Pkcs7Constants.OID_SIGNED_DATA.equals(contentInfo.contentType)) { + throw new Asn1DecodingException( + "Unsupported ContentInfo.contentType: " + contentInfo.contentType); + } + signedData = + Asn1BerParser.parse(contentInfo.content.getEncoded(), SignedData.class); + } catch (Asn1DecodingException e) { + e.printStackTrace(); + mResult.addError( + Issue.JAR_SIG_PARSE_EXCEPTION, mSignatureBlockEntry.getName(), e); + return; + } + + if (signedData.signerInfos.isEmpty()) { + mResult.addError(Issue.JAR_SIG_NO_SIGNERS, mSignatureBlockEntry.getName()); + return; + } + + // Find the first SignedData.SignerInfos element which verifies against the signature + // file + SignerInfo firstVerifiedSignerInfo = null; + X509Certificate firstVerifiedSignerInfoSigningCertificate = null; + // Prior to Android N, Android attempts to verify only the first SignerInfo. From N + // onwards, Android attempts to verify all SignerInfos and then picks the first verified + // SignerInfo. + List<SignerInfo> unverifiedSignerInfosToTry; + if (minSdkVersion < AndroidSdkVersion.N) { + unverifiedSignerInfosToTry = + Collections.singletonList(signedData.signerInfos.get(0)); + } else { + unverifiedSignerInfosToTry = signedData.signerInfos; + } + List<X509Certificate> signedDataCertificates = null; + for (SignerInfo unverifiedSignerInfo : unverifiedSignerInfosToTry) { + // Parse SignedData.certificates -- they are needed to verify SignerInfo + if (signedDataCertificates == null) { + try { + signedDataCertificates = parseCertificates(signedData.certificates); + } catch (CertificateException e) { + mResult.addError( + Issue.JAR_SIG_PARSE_EXCEPTION, mSignatureBlockEntry.getName(), e); + return; + } + } + + // Verify SignerInfo + X509Certificate signingCertificate; + try { + signingCertificate = + verifySignerInfoAgainstSigFile( + signedData, + signedDataCertificates, + unverifiedSignerInfo, + mSigFileBytes, + minSdkVersion, + maxSdkVersion); + if (mResult.containsErrors()) { + return; + } + if (signingCertificate != null) { + // SignerInfo verified + if (firstVerifiedSignerInfo == null) { + firstVerifiedSignerInfo = unverifiedSignerInfo; + firstVerifiedSignerInfoSigningCertificate = signingCertificate; + } + } + } catch (Pkcs7DecodingException e) { + mResult.addError( + Issue.JAR_SIG_PARSE_EXCEPTION, mSignatureBlockEntry.getName(), e); + return; + } catch (InvalidKeyException | SignatureException e) { + mResult.addError( + Issue.JAR_SIG_VERIFY_EXCEPTION, + mSignatureBlockEntry.getName(), + mSignatureFileEntry.getName(), + e); + return; + } + } + if (firstVerifiedSignerInfo == null) { + // No SignerInfo verified + mResult.addError( + Issue.JAR_SIG_DID_NOT_VERIFY, + mSignatureBlockEntry.getName(), + mSignatureFileEntry.getName()); + return; + } + // Verified + List<X509Certificate> signingCertChain = + getCertificateChain( + signedDataCertificates, firstVerifiedSignerInfoSigningCertificate); + mResult.certChain.clear(); + mResult.certChain.addAll(signingCertChain); + } + + /** + * Returns the signing certificate if the provided {@link SignerInfo} verifies against the + * contents of the provided signature file, or {@code null} if it does not verify. + */ + private X509Certificate verifySignerInfoAgainstSigFile( + SignedData signedData, + Collection<X509Certificate> signedDataCertificates, + SignerInfo signerInfo, + byte[] signatureFile, + int minSdkVersion, + int maxSdkVersion) + throws Pkcs7DecodingException, NoSuchAlgorithmException, + InvalidKeyException, SignatureException { + String digestAlgorithmOid = signerInfo.digestAlgorithm.algorithm; + String signatureAlgorithmOid = signerInfo.signatureAlgorithm.algorithm; + InclusiveIntRange desiredApiLevels = + InclusiveIntRange.fromTo(minSdkVersion, maxSdkVersion); + List<InclusiveIntRange> apiLevelsWhereDigestAndSigAlgorithmSupported = + getSigAlgSupportedApiLevels(digestAlgorithmOid, signatureAlgorithmOid); + List<InclusiveIntRange> apiLevelsWhereDigestAlgorithmNotSupported = + desiredApiLevels.getValuesNotIn(apiLevelsWhereDigestAndSigAlgorithmSupported); + if (!apiLevelsWhereDigestAlgorithmNotSupported.isEmpty()) { + String digestAlgorithmUserFriendly = + OidConstants.OidToUserFriendlyNameMapper.getUserFriendlyNameForOid( + digestAlgorithmOid); + if (digestAlgorithmUserFriendly == null) { + digestAlgorithmUserFriendly = digestAlgorithmOid; + } + String signatureAlgorithmUserFriendly = + OidConstants.OidToUserFriendlyNameMapper.getUserFriendlyNameForOid( + signatureAlgorithmOid); + if (signatureAlgorithmUserFriendly == null) { + signatureAlgorithmUserFriendly = signatureAlgorithmOid; + } + StringBuilder apiLevelsUserFriendly = new StringBuilder(); + for (InclusiveIntRange range : apiLevelsWhereDigestAlgorithmNotSupported) { + if (apiLevelsUserFriendly.length() > 0) { + apiLevelsUserFriendly.append(", "); + } + if (range.getMin() == range.getMax()) { + apiLevelsUserFriendly.append(String.valueOf(range.getMin())); + } else if (range.getMax() == Integer.MAX_VALUE) { + apiLevelsUserFriendly.append(range.getMin() + "+"); + } else { + apiLevelsUserFriendly.append(range.getMin() + "-" + range.getMax()); + } + } + mResult.addError( + Issue.JAR_SIG_UNSUPPORTED_SIG_ALG, + mSignatureBlockEntry.getName(), + digestAlgorithmOid, + signatureAlgorithmOid, + apiLevelsUserFriendly.toString(), + digestAlgorithmUserFriendly, + signatureAlgorithmUserFriendly); + return null; + } + + // From the bag of certs, obtain the certificate referenced by the SignerInfo, + // and verify the cryptographic signature in the SignerInfo against the certificate. + + // Locate the signing certificate referenced by the SignerInfo + X509Certificate signingCertificate = + findCertificate(signedDataCertificates, signerInfo.sid); + if (signingCertificate == null) { + throw new SignatureException( + "Signing certificate referenced in SignerInfo not found in" + + " SignedData"); + } + + // Check whether the signing certificate is acceptable. Android performs these + // checks explicitly, instead of delegating this to + // Signature.initVerify(Certificate). + if (signingCertificate.hasUnsupportedCriticalExtension()) { + throw new SignatureException( + "Signing certificate has unsupported critical extensions"); + } + boolean[] keyUsageExtension = signingCertificate.getKeyUsage(); + if (keyUsageExtension != null) { + boolean digitalSignature = + (keyUsageExtension.length >= 1) && (keyUsageExtension[0]); + boolean nonRepudiation = + (keyUsageExtension.length >= 2) && (keyUsageExtension[1]); + if ((!digitalSignature) && (!nonRepudiation)) { + throw new SignatureException( + "Signing certificate not authorized for use in digital signatures" + + ": keyUsage extension missing digitalSignature and" + + " nonRepudiation"); + } + } + + // Verify the cryptographic signature in SignerInfo against the certificate's + // public key + String jcaSignatureAlgorithm = + getJcaSignatureAlgorithm(digestAlgorithmOid, signatureAlgorithmOid); + Signature s = Signature.getInstance(jcaSignatureAlgorithm); + PublicKey publicKey = signingCertificate.getPublicKey(); + try { + s.initVerify(publicKey); + } catch (InvalidKeyException e) { + // An InvalidKeyException could be caught if the PublicKey in the certificate is not + // properly encoded; attempt to resolve any encoding errors, generate a new public + // key, and reattempt the initVerify with the newly encoded key. + try { + byte[] encodedPublicKey = ApkSigningBlockUtils.encodePublicKey(publicKey); + publicKey = KeyFactory.getInstance(publicKey.getAlgorithm()).generatePublic( + new X509EncodedKeySpec(encodedPublicKey)); + } catch (InvalidKeySpecException ikse) { + // If an InvalidKeySpecException is caught then throw the original Exception + // since the key couldn't be properly re-encoded, and the original Exception + // will have more useful debugging info. + throw e; + } + s = Signature.getInstance(jcaSignatureAlgorithm); + s.initVerify(publicKey); + } + + if (signerInfo.signedAttrs != null) { + // Signed attributes present -- verify signature against the ASN.1 DER encoded form + // of signed attributes. This verifies integrity of the signature file because + // signed attributes must contain the digest of the signature file. + if (minSdkVersion < AndroidSdkVersion.KITKAT) { + // Prior to Android KitKat, APKs with signed attributes are unsafe: + // * The APK's contents are not protected by the JAR signature because the + // digest in signed attributes is not verified. This means an attacker can + // arbitrarily modify the APK without invalidating its signature. + // * Luckily, the signature over signed attributes was verified incorrectly + // (over the verbatim IMPLICIT [0] form rather than over re-encoded + // UNIVERSAL SET form) which means that JAR signatures which would verify on + // pre-KitKat Android and yet do not protect the APK from modification could + // be generated only by broken tools or on purpose by the entity signing the + // APK. + // + // We thus reject such unsafe APKs, even if they verify on platforms before + // KitKat. + throw new SignatureException( + "APKs with Signed Attributes broken on platforms with API Level < " + + AndroidSdkVersion.KITKAT); + } + try { + List<Attribute> signedAttributes = + Asn1BerParser.parseImplicitSetOf( + signerInfo.signedAttrs.getEncoded(), Attribute.class); + SignedAttributes signedAttrs = new SignedAttributes(signedAttributes); + if (maxSdkVersion >= AndroidSdkVersion.N) { + // Content Type attribute is checked only on Android N and newer + String contentType = + signedAttrs.getSingleObjectIdentifierValue( + Pkcs7Constants.OID_CONTENT_TYPE); + if (contentType == null) { + throw new SignatureException("No Content Type in signed attributes"); + } + if (!contentType.equals(signedData.encapContentInfo.contentType)) { + // Did not verify: Content type signed attribute does not match + // SignedData.encapContentInfo.eContentType. This fails verification of + // this SignerInfo but should not prevent verification of other + // SignerInfos. Hence, no exception is thrown. + return null; + } + } + byte[] expectedSignatureFileDigest = + signedAttrs.getSingleOctetStringValue( + Pkcs7Constants.OID_MESSAGE_DIGEST); + if (expectedSignatureFileDigest == null) { + throw new SignatureException("No content digest in signed attributes"); + } + byte[] actualSignatureFileDigest = + MessageDigest.getInstance( + getJcaDigestAlgorithm(digestAlgorithmOid)) + .digest(signatureFile); + if (!Arrays.equals( + expectedSignatureFileDigest, actualSignatureFileDigest)) { + // Skip verification: signature file digest in signed attributes does not + // match the signature file. This fails verification of + // this SignerInfo but should not prevent verification of other + // SignerInfos. Hence, no exception is thrown. + return null; + } + } catch (Asn1DecodingException e) { + throw new SignatureException("Failed to parse signed attributes", e); + } + // PKCS #7 requires that signature is over signed attributes re-encoded as + // ASN.1 DER. However, Android does not re-encode except for changing the + // first byte of encoded form from IMPLICIT [0] to UNIVERSAL SET. We do the + // same for maximum compatibility. + ByteBuffer signedAttrsOriginalEncoding = signerInfo.signedAttrs.getEncoded(); + s.update((byte) 0x31); // UNIVERSAL SET + signedAttrsOriginalEncoding.position(1); + s.update(signedAttrsOriginalEncoding); + } else { + // No signed attributes present -- verify signature against the contents of the + // signature file + s.update(signatureFile); + } + byte[] sigBytes = ByteBufferUtils.toByteArray(signerInfo.signature.slice()); + if (!s.verify(sigBytes)) { + // Cryptographic signature did not verify. This fails verification of this + // SignerInfo but should not prevent verification of other SignerInfos. Hence, no + // exception is thrown. + return null; + } + // Cryptographic signature verified + return signingCertificate; + } + + + + public static List<X509Certificate> getCertificateChain( + List<X509Certificate> certs, X509Certificate leaf) { + List<X509Certificate> unusedCerts = new ArrayList<>(certs); + List<X509Certificate> result = new ArrayList<>(1); + result.add(leaf); + unusedCerts.remove(leaf); + X509Certificate root = leaf; + while (!root.getSubjectDN().equals(root.getIssuerDN())) { + Principal targetDn = root.getIssuerDN(); + boolean issuerFound = false; + for (int i = 0; i < unusedCerts.size(); i++) { + X509Certificate unusedCert = unusedCerts.get(i); + if (targetDn.equals(unusedCert.getSubjectDN())) { + issuerFound = true; + unusedCerts.remove(i); + result.add(unusedCert); + root = unusedCert; + break; + } + } + if (!issuerFound) { + break; + } + } + return result; + } + + + + + public void verifySigFileAgainstManifest( + byte[] manifestBytes, + ManifestParser.Section manifestMainSection, + Map<String, ManifestParser.Section> entryNameToManifestSection, + Map<Integer, String> supportedApkSigSchemeNames, + Set<Integer> foundApkSigSchemeIds, + int minSdkVersion, + int maxSdkVersion) throws NoSuchAlgorithmException { + // Inspect the main section of the .SF file. + ManifestParser sf = new ManifestParser(mSigFileBytes); + ManifestParser.Section sfMainSection = sf.readSection(); + if (sfMainSection.getAttributeValue(Attributes.Name.SIGNATURE_VERSION) == null) { + mResult.addError( + Issue.JAR_SIG_MISSING_VERSION_ATTR_IN_SIG_FILE, + mSignatureFileEntry.getName()); + setIgnored(); + return; + } + + if (maxSdkVersion >= AndroidSdkVersion.N) { + // Android N and newer rejects APKs whose .SF file says they were supposed to be + // signed with APK Signature Scheme v2 (or newer) and yet no such signature was + // found. + checkForStrippedApkSignatures( + sfMainSection, supportedApkSigSchemeNames, foundApkSigSchemeIds); + if (mResult.containsErrors()) { + return; + } + } + + boolean createdBySigntool = false; + String createdBy = sfMainSection.getAttributeValue("Created-By"); + if (createdBy != null) { + createdBySigntool = createdBy.indexOf("signtool") != -1; + } + boolean manifestDigestVerified = + verifyManifestDigest( + sfMainSection, + createdBySigntool, + manifestBytes, + minSdkVersion, + maxSdkVersion); + if (!createdBySigntool) { + verifyManifestMainSectionDigest( + sfMainSection, + manifestMainSection, + manifestBytes, + minSdkVersion, + maxSdkVersion); + } + if (mResult.containsErrors()) { + return; + } + + // Inspect per-entry sections of .SF file. Technically, if the digest of JAR manifest + // verifies, per-entry sections should be ignored. However, most Android platform + // implementations require that such sections exist. + List<ManifestParser.Section> sfSections = sf.readAllSections(); + Set<String> sfEntryNames = new HashSet<>(sfSections.size()); + int sfSectionNumber = 0; + for (ManifestParser.Section sfSection : sfSections) { + sfSectionNumber++; + String entryName = sfSection.getName(); + if (entryName == null) { + mResult.addError( + Issue.JAR_SIG_UNNNAMED_SIG_FILE_SECTION, + mSignatureFileEntry.getName(), + sfSectionNumber); + setIgnored(); + return; + } + if (!sfEntryNames.add(entryName)) { + mResult.addError( + Issue.JAR_SIG_DUPLICATE_SIG_FILE_SECTION, + mSignatureFileEntry.getName(), + entryName); + setIgnored(); + return; + } + if (manifestDigestVerified) { + // No need to verify this entry's corresponding JAR manifest entry because the + // JAR manifest verifies in full. + continue; + } + // Whole-file digest of JAR manifest hasn't been verified. Thus, we need to verify + // the digest of the JAR manifest section corresponding to this .SF section. + ManifestParser.Section manifestSection = entryNameToManifestSection.get(entryName); + if (manifestSection == null) { + mResult.addError( + Issue.JAR_SIG_NO_ZIP_ENTRY_DIGEST_IN_SIG_FILE, + entryName, + mSignatureFileEntry.getName()); + setIgnored(); + continue; + } + verifyManifestIndividualSectionDigest( + sfSection, + createdBySigntool, + manifestSection, + manifestBytes, + minSdkVersion, + maxSdkVersion); + } + mSigFileEntryNames = sfEntryNames; + } + + + /** + * Returns {@code true} if the whole-file digest of the manifest against the main section of + * the .SF file. + */ + private boolean verifyManifestDigest( + ManifestParser.Section sfMainSection, + boolean createdBySigntool, + byte[] manifestBytes, + int minSdkVersion, + int maxSdkVersion) throws NoSuchAlgorithmException { + Collection<NamedDigest> expectedDigests = + getDigestsToVerify( + sfMainSection, + ((createdBySigntool) ? "-Digest" : "-Digest-Manifest"), + minSdkVersion, + maxSdkVersion); + boolean digestFound = !expectedDigests.isEmpty(); + if (!digestFound) { + mResult.addWarning( + Issue.JAR_SIG_NO_MANIFEST_DIGEST_IN_SIG_FILE, + mSignatureFileEntry.getName()); + return false; + } + + boolean verified = true; + for (NamedDigest expectedDigest : expectedDigests) { + String jcaDigestAlgorithm = expectedDigest.jcaDigestAlgorithm; + byte[] actual = digest(jcaDigestAlgorithm, manifestBytes); + byte[] expected = expectedDigest.digest; + if (!Arrays.equals(expected, actual)) { + mResult.addWarning( + Issue.JAR_SIG_ZIP_ENTRY_DIGEST_DID_NOT_VERIFY, + V1SchemeConstants.MANIFEST_ENTRY_NAME, + jcaDigestAlgorithm, + mSignatureFileEntry.getName(), + Base64.getEncoder().encodeToString(actual), + Base64.getEncoder().encodeToString(expected)); + verified = false; + } + } + return verified; + } + + /** + * Verifies the digest of the manifest's main section against the main section of the .SF + * file. + */ + private void verifyManifestMainSectionDigest( + ManifestParser.Section sfMainSection, + ManifestParser.Section manifestMainSection, + byte[] manifestBytes, + int minSdkVersion, + int maxSdkVersion) throws NoSuchAlgorithmException { + Collection<NamedDigest> expectedDigests = + getDigestsToVerify( + sfMainSection, + "-Digest-Manifest-Main-Attributes", + minSdkVersion, + maxSdkVersion); + if (expectedDigests.isEmpty()) { + return; + } + + for (NamedDigest expectedDigest : expectedDigests) { + String jcaDigestAlgorithm = expectedDigest.jcaDigestAlgorithm; + byte[] actual = + digest( + jcaDigestAlgorithm, + manifestBytes, + manifestMainSection.getStartOffset(), + manifestMainSection.getSizeBytes()); + byte[] expected = expectedDigest.digest; + if (!Arrays.equals(expected, actual)) { + mResult.addError( + Issue.JAR_SIG_MANIFEST_MAIN_SECTION_DIGEST_DID_NOT_VERIFY, + jcaDigestAlgorithm, + mSignatureFileEntry.getName(), + Base64.getEncoder().encodeToString(actual), + Base64.getEncoder().encodeToString(expected)); + } + } + } + + /** + * Verifies the digest of the manifest's individual section against the corresponding + * individual section of the .SF file. + */ + private void verifyManifestIndividualSectionDigest( + ManifestParser.Section sfIndividualSection, + boolean createdBySigntool, + ManifestParser.Section manifestIndividualSection, + byte[] manifestBytes, + int minSdkVersion, + int maxSdkVersion) throws NoSuchAlgorithmException { + String entryName = sfIndividualSection.getName(); + Collection<NamedDigest> expectedDigests = + getDigestsToVerify( + sfIndividualSection, "-Digest", minSdkVersion, maxSdkVersion); + if (expectedDigests.isEmpty()) { + mResult.addError( + Issue.JAR_SIG_NO_ZIP_ENTRY_DIGEST_IN_SIG_FILE, + entryName, + mSignatureFileEntry.getName()); + return; + } + + int sectionStartIndex = manifestIndividualSection.getStartOffset(); + int sectionSizeBytes = manifestIndividualSection.getSizeBytes(); + if (createdBySigntool) { + int sectionEndIndex = sectionStartIndex + sectionSizeBytes; + if ((manifestBytes[sectionEndIndex - 1] == '\n') + && (manifestBytes[sectionEndIndex - 2] == '\n')) { + sectionSizeBytes--; + } + } + for (NamedDigest expectedDigest : expectedDigests) { + String jcaDigestAlgorithm = expectedDigest.jcaDigestAlgorithm; + byte[] actual = + digest( + jcaDigestAlgorithm, + manifestBytes, + sectionStartIndex, + sectionSizeBytes); + byte[] expected = expectedDigest.digest; + if (!Arrays.equals(expected, actual)) { + mResult.addError( + Issue.JAR_SIG_MANIFEST_SECTION_DIGEST_DID_NOT_VERIFY, + entryName, + jcaDigestAlgorithm, + mSignatureFileEntry.getName(), + Base64.getEncoder().encodeToString(actual), + Base64.getEncoder().encodeToString(expected)); + } + } + } + + private void checkForStrippedApkSignatures( + ManifestParser.Section sfMainSection, + Map<Integer, String> supportedApkSigSchemeNames, + Set<Integer> foundApkSigSchemeIds) { + String signedWithApkSchemes = + sfMainSection.getAttributeValue( + V1SchemeConstants.SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME_STR); + // This field contains a comma-separated list of APK signature scheme IDs which were + // used to sign this APK. Android rejects APKs where an ID is known to the platform but + // the APK didn't verify using that scheme. + + if (signedWithApkSchemes == null) { + // APK signature (e.g., v2 scheme) stripping protections not enabled. + if (!foundApkSigSchemeIds.isEmpty()) { + // APK is signed with an APK signature scheme such as v2 scheme. + mResult.addWarning( + Issue.JAR_SIG_NO_APK_SIG_STRIP_PROTECTION, + mSignatureFileEntry.getName()); + } + return; + } + + if (supportedApkSigSchemeNames.isEmpty()) { + return; + } + + Set<Integer> supportedApkSigSchemeIds = supportedApkSigSchemeNames.keySet(); + Set<Integer> supportedExpectedApkSigSchemeIds = new HashSet<>(1); + StringTokenizer tokenizer = new StringTokenizer(signedWithApkSchemes, ","); + while (tokenizer.hasMoreTokens()) { + String idText = tokenizer.nextToken().trim(); + if (idText.isEmpty()) { + continue; + } + int id; + try { + id = Integer.parseInt(idText); + } catch (Exception ignored) { + continue; + } + // This APK was supposed to be signed with the APK signature scheme having + // this ID. + if (supportedApkSigSchemeIds.contains(id)) { + supportedExpectedApkSigSchemeIds.add(id); + } else { + mResult.addWarning( + Issue.JAR_SIG_UNKNOWN_APK_SIG_SCHEME_ID, + mSignatureFileEntry.getName(), + id); + } + } + + for (int id : supportedExpectedApkSigSchemeIds) { + if (!foundApkSigSchemeIds.contains(id)) { + String apkSigSchemeName = supportedApkSigSchemeNames.get(id); + mResult.addError( + Issue.JAR_SIG_MISSING_APK_SIG_REFERENCED, + mSignatureFileEntry.getName(), + id, + apkSigSchemeName); + } + } + } + } + + public static Collection<NamedDigest> getDigestsToVerify( + ManifestParser.Section section, + String digestAttrSuffix, + int minSdkVersion, + int maxSdkVersion) { + Decoder base64Decoder = Base64.getDecoder(); + List<NamedDigest> result = new ArrayList<>(1); + if (minSdkVersion < AndroidSdkVersion.JELLY_BEAN_MR2) { + // Prior to JB MR2, Android platform's logic for picking a digest algorithm to verify is + // to rely on the ancient Digest-Algorithms attribute which contains + // whitespace-separated list of digest algorithms (defaulting to SHA-1) to try. The + // first digest attribute (with supported digest algorithm) found using the list is + // used. + String algs = section.getAttributeValue("Digest-Algorithms"); + if (algs == null) { + algs = "SHA SHA1"; + } + StringTokenizer tokens = new StringTokenizer(algs); + while (tokens.hasMoreTokens()) { + String alg = tokens.nextToken(); + String attrName = alg + digestAttrSuffix; + String digestBase64 = section.getAttributeValue(attrName); + if (digestBase64 == null) { + // Attribute not found + continue; + } + alg = getCanonicalJcaMessageDigestAlgorithm(alg); + if ((alg == null) + || (getMinSdkVersionFromWhichSupportedInManifestOrSignatureFile(alg) + > minSdkVersion)) { + // Unsupported digest algorithm + continue; + } + // Supported digest algorithm + result.add(new NamedDigest(alg, base64Decoder.decode(digestBase64))); + break; + } + // No supported digests found -- this will fail to verify on pre-JB MR2 Androids. + if (result.isEmpty()) { + return result; + } + } + + if (maxSdkVersion >= AndroidSdkVersion.JELLY_BEAN_MR2) { + // On JB MR2 and newer, Android platform picks the strongest algorithm out of: + // SHA-512, SHA-384, SHA-256, SHA-1. + for (String alg : JB_MR2_AND_NEWER_DIGEST_ALGS) { + String attrName = getJarDigestAttributeName(alg, digestAttrSuffix); + String digestBase64 = section.getAttributeValue(attrName); + if (digestBase64 == null) { + // Attribute not found + continue; + } + byte[] digest = base64Decoder.decode(digestBase64); + byte[] digestInResult = getDigest(result, alg); + if ((digestInResult == null) || (!Arrays.equals(digestInResult, digest))) { + result.add(new NamedDigest(alg, digest)); + } + break; + } + } + + return result; + } + + private static final String[] JB_MR2_AND_NEWER_DIGEST_ALGS = { + "SHA-512", + "SHA-384", + "SHA-256", + "SHA-1", + }; + + private static String getCanonicalJcaMessageDigestAlgorithm(String algorithm) { + return UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL.get(algorithm.toUpperCase(Locale.US)); + } + + public static int getMinSdkVersionFromWhichSupportedInManifestOrSignatureFile( + String jcaAlgorithmName) { + Integer result = + MIN_SDK_VESION_FROM_WHICH_DIGEST_SUPPORTED_IN_MANIFEST.get( + jcaAlgorithmName.toUpperCase(Locale.US)); + return (result != null) ? result : Integer.MAX_VALUE; + } + + private static String getJarDigestAttributeName( + String jcaDigestAlgorithm, String attrNameSuffix) { + if ("SHA-1".equalsIgnoreCase(jcaDigestAlgorithm)) { + return "SHA1" + attrNameSuffix; + } else { + return jcaDigestAlgorithm + attrNameSuffix; + } + } + + private static final Map<String, String> UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL; + static { + UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL = new HashMap<>(8); + UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL.put("MD5", "MD5"); + UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL.put("SHA", "SHA-1"); + UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL.put("SHA1", "SHA-1"); + UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL.put("SHA-1", "SHA-1"); + UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL.put("SHA-256", "SHA-256"); + UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL.put("SHA-384", "SHA-384"); + UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL.put("SHA-512", "SHA-512"); + } + + private static final Map<String, Integer> + MIN_SDK_VESION_FROM_WHICH_DIGEST_SUPPORTED_IN_MANIFEST; + static { + MIN_SDK_VESION_FROM_WHICH_DIGEST_SUPPORTED_IN_MANIFEST = new HashMap<>(5); + MIN_SDK_VESION_FROM_WHICH_DIGEST_SUPPORTED_IN_MANIFEST.put("MD5", 0); + MIN_SDK_VESION_FROM_WHICH_DIGEST_SUPPORTED_IN_MANIFEST.put("SHA-1", 0); + MIN_SDK_VESION_FROM_WHICH_DIGEST_SUPPORTED_IN_MANIFEST.put("SHA-256", 0); + MIN_SDK_VESION_FROM_WHICH_DIGEST_SUPPORTED_IN_MANIFEST.put( + "SHA-384", AndroidSdkVersion.GINGERBREAD); + MIN_SDK_VESION_FROM_WHICH_DIGEST_SUPPORTED_IN_MANIFEST.put( + "SHA-512", AndroidSdkVersion.GINGERBREAD); + } + + private static byte[] getDigest(Collection<NamedDigest> digests, String jcaDigestAlgorithm) { + for (NamedDigest digest : digests) { + if (digest.jcaDigestAlgorithm.equalsIgnoreCase(jcaDigestAlgorithm)) { + return digest.digest; + } + } + return null; + } + + public static List<CentralDirectoryRecord> parseZipCentralDirectory( + DataSource apk, + ApkUtils.ZipSections apkSections) + throws IOException, ApkFormatException { + return ZipUtils.parseZipCentralDirectory(apk, apkSections); + } + + /** + * Returns {@code true} if the provided JAR entry must be mentioned in signed JAR archive's + * manifest for the APK to verify on Android. + */ + private static boolean isJarEntryDigestNeededInManifest(String entryName) { + // NOTE: This logic is different from what's required by the JAR signing scheme. This is + // because Android's APK verification logic differs from that spec. In particular, JAR + // signing spec includes into JAR manifest all files in subdirectories of META-INF and + // any files inside META-INF not related to signatures. + if (entryName.startsWith("META-INF/")) { + return false; + } + return !entryName.endsWith("/"); + } + + private static Set<Signer> verifyJarEntriesAgainstManifestAndSigners( + DataSource apk, + long cdOffsetInApk, + Collection<CentralDirectoryRecord> cdRecords, + Map<String, ManifestParser.Section> entryNameToManifestSection, + List<Signer> signers, + int minSdkVersion, + int maxSdkVersion, + Result result) throws ApkFormatException, IOException, NoSuchAlgorithmException { + // Iterate over APK contents as sequentially as possible to improve performance. + List<CentralDirectoryRecord> cdRecordsSortedByLocalFileHeaderOffset = + new ArrayList<>(cdRecords); + Collections.sort( + cdRecordsSortedByLocalFileHeaderOffset, + CentralDirectoryRecord.BY_LOCAL_FILE_HEADER_OFFSET_COMPARATOR); + List<Signer> firstSignedEntrySigners = null; + String firstSignedEntryName = null; + for (CentralDirectoryRecord cdRecord : cdRecordsSortedByLocalFileHeaderOffset) { + String entryName = cdRecord.getName(); + if (!isJarEntryDigestNeededInManifest(entryName)) { + continue; + } + + ManifestParser.Section manifestSection = entryNameToManifestSection.get(entryName); + if (manifestSection == null) { + result.addError(Issue.JAR_SIG_NO_ZIP_ENTRY_DIGEST_IN_MANIFEST, entryName); + continue; + } + + List<Signer> entrySigners = new ArrayList<>(signers.size()); + for (Signer signer : signers) { + if (signer.getSigFileEntryNames().contains(entryName)) { + entrySigners.add(signer); + } + } + if (entrySigners.isEmpty()) { + result.addError(Issue.JAR_SIG_ZIP_ENTRY_NOT_SIGNED, entryName); + continue; + } + if (firstSignedEntrySigners == null) { + firstSignedEntrySigners = entrySigners; + firstSignedEntryName = entryName; + } else if (!entrySigners.equals(firstSignedEntrySigners)) { + result.addError( + Issue.JAR_SIG_ZIP_ENTRY_SIGNERS_MISMATCH, + firstSignedEntryName, + getSignerNames(firstSignedEntrySigners), + entryName, + getSignerNames(entrySigners)); + continue; + } + + List<NamedDigest> expectedDigests = + new ArrayList<>( + getDigestsToVerify( + manifestSection, "-Digest", minSdkVersion, maxSdkVersion)); + if (expectedDigests.isEmpty()) { + result.addError(Issue.JAR_SIG_NO_ZIP_ENTRY_DIGEST_IN_MANIFEST, entryName); + continue; + } + + MessageDigest[] mds = new MessageDigest[expectedDigests.size()]; + for (int i = 0; i < expectedDigests.size(); i++) { + mds[i] = getMessageDigest(expectedDigests.get(i).jcaDigestAlgorithm); + } + + try { + LocalFileRecord.outputUncompressedData( + apk, + cdRecord, + cdOffsetInApk, + DataSinks.asDataSink(mds)); + } catch (ZipFormatException e) { + throw new ApkFormatException("Malformed ZIP entry: " + entryName, e); + } catch (IOException e) { + throw new IOException("Failed to read entry: " + entryName, e); + } + + for (int i = 0; i < expectedDigests.size(); i++) { + NamedDigest expectedDigest = expectedDigests.get(i); + byte[] actualDigest = mds[i].digest(); + if (!Arrays.equals(expectedDigest.digest, actualDigest)) { + result.addError( + Issue.JAR_SIG_ZIP_ENTRY_DIGEST_DID_NOT_VERIFY, + entryName, + expectedDigest.jcaDigestAlgorithm, + V1SchemeConstants.MANIFEST_ENTRY_NAME, + Base64.getEncoder().encodeToString(actualDigest), + Base64.getEncoder().encodeToString(expectedDigest.digest)); + } + } + } + + if (firstSignedEntrySigners == null) { + result.addError(Issue.JAR_SIG_NO_SIGNED_ZIP_ENTRIES); + return Collections.emptySet(); + } else { + return new HashSet<>(firstSignedEntrySigners); + } + } + + private static List<String> getSignerNames(List<Signer> signers) { + if (signers.isEmpty()) { + return Collections.emptyList(); + } + List<String> result = new ArrayList<>(signers.size()); + for (Signer signer : signers) { + result.add(signer.getName()); + } + return result; + } + + private static MessageDigest getMessageDigest(String algorithm) + throws NoSuchAlgorithmException { + return MessageDigest.getInstance(algorithm); + } + + private static byte[] digest(String algorithm, byte[] data, int offset, int length) + throws NoSuchAlgorithmException { + MessageDigest md = getMessageDigest(algorithm); + md.update(data, offset, length); + return md.digest(); + } + + private static byte[] digest(String algorithm, byte[] data) throws NoSuchAlgorithmException { + return getMessageDigest(algorithm).digest(data); + } + + public static class NamedDigest { + public final String jcaDigestAlgorithm; + public final byte[] digest; + + private NamedDigest(String jcaDigestAlgorithm, byte[] digest) { + this.jcaDigestAlgorithm = jcaDigestAlgorithm; + this.digest = digest; + } + } + + public static class Result { + + /** Whether the APK's JAR signature verifies. */ + public boolean verified; + + /** List of APK's signers. These signers are used by Android. */ + public final List<SignerInfo> signers = new ArrayList<>(); + + /** + * Signers encountered in the APK but not included in the set of the APK's signers. These + * signers are ignored by Android. + */ + public final List<SignerInfo> ignoredSigners = new ArrayList<>(); + + private final List<IssueWithParams> mWarnings = new ArrayList<>(); + private final List<IssueWithParams> mErrors = new ArrayList<>(); + + private boolean containsErrors() { + if (!mErrors.isEmpty()) { + return true; + } + for (SignerInfo signer : signers) { + if (signer.containsErrors()) { + return true; + } + } + return false; + } + + private void addError(Issue msg, Object... parameters) { + mErrors.add(new IssueWithParams(msg, parameters)); + } + + private void addWarning(Issue msg, Object... parameters) { + mWarnings.add(new IssueWithParams(msg, parameters)); + } + + public List<IssueWithParams> getErrors() { + return mErrors; + } + + public List<IssueWithParams> getWarnings() { + return mWarnings; + } + + public static class SignerInfo { + public final String name; + public final String signatureFileName; + public final String signatureBlockFileName; + public final List<X509Certificate> certChain = new ArrayList<>(); + + private final List<IssueWithParams> mWarnings = new ArrayList<>(); + private final List<IssueWithParams> mErrors = new ArrayList<>(); + + private SignerInfo( + String name, String signatureBlockFileName, String signatureFileName) { + this.name = name; + this.signatureBlockFileName = signatureBlockFileName; + this.signatureFileName = signatureFileName; + } + + private boolean containsErrors() { + return !mErrors.isEmpty(); + } + + private void addError(Issue msg, Object... parameters) { + mErrors.add(new IssueWithParams(msg, parameters)); + } + + private void addWarning(Issue msg, Object... parameters) { + mWarnings.add(new IssueWithParams(msg, parameters)); + } + + public List<IssueWithParams> getErrors() { + return mErrors; + } + + public List<IssueWithParams> getWarnings() { + return mWarnings; + } + } + } + + private static class SignedAttributes { + private Map<String, List<Asn1OpaqueObject>> mAttrs; + + public SignedAttributes(Collection<Attribute> attrs) throws Pkcs7DecodingException { + Map<String, List<Asn1OpaqueObject>> result = new HashMap<>(attrs.size()); + for (Attribute attr : attrs) { + if (result.put(attr.attrType, attr.attrValues) != null) { + throw new Pkcs7DecodingException("Duplicate signed attribute: " + attr.attrType); + } + } + mAttrs = result; + } + + private Asn1OpaqueObject getSingleValue(String attrOid) throws Pkcs7DecodingException { + List<Asn1OpaqueObject> values = mAttrs.get(attrOid); + if ((values == null) || (values.isEmpty())) { + return null; + } + if (values.size() > 1) { + throw new Pkcs7DecodingException("Attribute " + attrOid + " has multiple values"); + } + return values.get(0); + } + + public String getSingleObjectIdentifierValue(String attrOid) throws Pkcs7DecodingException { + Asn1OpaqueObject value = getSingleValue(attrOid); + if (value == null) { + return null; + } + try { + return Asn1BerParser.parse(value.getEncoded(), ObjectIdentifierChoice.class).value; + } catch (Asn1DecodingException e) { + throw new Pkcs7DecodingException("Failed to decode OBJECT IDENTIFIER", e); + } + } + + public byte[] getSingleOctetStringValue(String attrOid) throws Pkcs7DecodingException { + Asn1OpaqueObject value = getSingleValue(attrOid); + if (value == null) { + return null; + } + try { + return Asn1BerParser.parse(value.getEncoded(), OctetStringChoice.class).value; + } catch (Asn1DecodingException e) { + throw new Pkcs7DecodingException("Failed to decode OBJECT IDENTIFIER", e); + } + } + } + + @Asn1Class(type = Asn1Type.CHOICE) + public static class OctetStringChoice { + @Asn1Field(type = Asn1Type.OCTET_STRING) + public byte[] value; + } + + @Asn1Class(type = Asn1Type.CHOICE) + public static class ObjectIdentifierChoice { + @Asn1Field(type = Asn1Type.OBJECT_IDENTIFIER) + public String value; + } +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeConstants.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeConstants.java new file mode 100644 index 0000000000..0e244c8373 --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeConstants.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.apk.v2; + +/** Constants used by the V2 Signature Scheme signing and verification. */ +public class V2SchemeConstants { + private V2SchemeConstants() {} + + public static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID = 0x7109871a; + public static final int STRIPPING_PROTECTION_ATTR_ID = 0xbeeff00d; +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeSigner.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeSigner.java new file mode 100644 index 0000000000..06da96cf20 --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeSigner.java @@ -0,0 +1,329 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.apk.v2; + +import static com.android.apksig.Constants.MAX_APK_SIGNERS; +import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedElements; +import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes; +import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeCertificates; +import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodePublicKey; + +import com.android.apksig.internal.apk.ApkSigningBlockUtils; +import com.android.apksig.internal.apk.ApkSigningBlockUtils.SignerConfig; +import com.android.apksig.internal.apk.ContentDigestAlgorithm; +import com.android.apksig.internal.apk.SignatureAlgorithm; +import com.android.apksig.internal.util.Pair; +import com.android.apksig.util.DataSource; +import com.android.apksig.util.RunnablesExecutor; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.SignatureException; +import java.security.cert.CertificateEncodingException; +import java.security.interfaces.ECKey; +import java.security.interfaces.RSAKey; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** + * APK Signature Scheme v2 signer. + * + * <p>APK Signature Scheme v2 is a whole-file signature scheme which aims to protect every single + * bit of the APK, as opposed to the JAR Signature Scheme which protects only the names and + * uncompressed contents of ZIP entries. + * + * @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2</a> + */ +public abstract class V2SchemeSigner { + /* + * The two main goals of APK Signature Scheme v2 are: + * 1. Detect any unauthorized modifications to the APK. This is achieved by making the signature + * cover every byte of the APK being signed. + * 2. Enable much faster signature and integrity verification. This is achieved by requiring + * only a minimal amount of APK parsing before the signature is verified, thus completely + * bypassing ZIP entry decompression and by making integrity verification parallelizable by + * employing a hash tree. + * + * The generated signature block is wrapped into an APK Signing Block and inserted into the + * original APK immediately before the start of ZIP Central Directory. This is to ensure that + * JAR and ZIP parsers continue to work on the signed APK. The APK Signing Block is designed for + * extensibility. For example, a future signature scheme could insert its signatures there as + * well. The contract of the APK Signing Block is that all contents outside of the block must be + * protected by signatures inside the block. + */ + + public static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID = + V2SchemeConstants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID; + + /** Hidden constructor to prevent instantiation. */ + private V2SchemeSigner() {} + + /** + * Gets the APK Signature Scheme v2 signature algorithms to be used for signing an APK using the + * provided key. + * + * @param minSdkVersion minimum API Level of the platform on which the APK may be installed (see + * AndroidManifest.xml minSdkVersion attribute). + * @throws InvalidKeyException if the provided key is not suitable for signing APKs using APK + * Signature Scheme v2 + */ + public static List<SignatureAlgorithm> getSuggestedSignatureAlgorithms(PublicKey signingKey, + int minSdkVersion, boolean verityEnabled, boolean deterministicDsaSigning) + throws InvalidKeyException { + String keyAlgorithm = signingKey.getAlgorithm(); + if ("RSA".equalsIgnoreCase(keyAlgorithm)) { + // Use RSASSA-PKCS1-v1_5 signature scheme instead of RSASSA-PSS to guarantee + // deterministic signatures which make life easier for OTA updates (fewer files + // changed when deterministic signature schemes are used). + + // Pick a digest which is no weaker than the key. + int modulusLengthBits = ((RSAKey) signingKey).getModulus().bitLength(); + if (modulusLengthBits <= 3072) { + // 3072-bit RSA is roughly 128-bit strong, meaning SHA-256 is a good fit. + List<SignatureAlgorithm> algorithms = new ArrayList<>(); + algorithms.add(SignatureAlgorithm.RSA_PKCS1_V1_5_WITH_SHA256); + if (verityEnabled) { + algorithms.add(SignatureAlgorithm.VERITY_RSA_PKCS1_V1_5_WITH_SHA256); + } + return algorithms; + } else { + // Keys longer than 3072 bit need to be paired with a stronger digest to avoid the + // digest being the weak link. SHA-512 is the next strongest supported digest. + return Collections.singletonList(SignatureAlgorithm.RSA_PKCS1_V1_5_WITH_SHA512); + } + } else if ("DSA".equalsIgnoreCase(keyAlgorithm)) { + // DSA is supported only with SHA-256. + List<SignatureAlgorithm> algorithms = new ArrayList<>(); + algorithms.add( + deterministicDsaSigning ? + SignatureAlgorithm.DETDSA_WITH_SHA256 : + SignatureAlgorithm.DSA_WITH_SHA256); + if (verityEnabled) { + algorithms.add(SignatureAlgorithm.VERITY_DSA_WITH_SHA256); + } + return algorithms; + } else if ("EC".equalsIgnoreCase(keyAlgorithm)) { + // Pick a digest which is no weaker than the key. + int keySizeBits = ((ECKey) signingKey).getParams().getOrder().bitLength(); + if (keySizeBits <= 256) { + // 256-bit Elliptic Curve is roughly 128-bit strong, meaning SHA-256 is a good fit. + List<SignatureAlgorithm> algorithms = new ArrayList<>(); + algorithms.add(SignatureAlgorithm.ECDSA_WITH_SHA256); + if (verityEnabled) { + algorithms.add(SignatureAlgorithm.VERITY_ECDSA_WITH_SHA256); + } + return algorithms; + } else { + // Keys longer than 256 bit need to be paired with a stronger digest to avoid the + // digest being the weak link. SHA-512 is the next strongest supported digest. + return Collections.singletonList(SignatureAlgorithm.ECDSA_WITH_SHA512); + } + } else { + throw new InvalidKeyException("Unsupported key algorithm: " + keyAlgorithm); + } + } + + public static ApkSigningBlockUtils.SigningSchemeBlockAndDigests + generateApkSignatureSchemeV2Block(RunnablesExecutor executor, + DataSource beforeCentralDir, + DataSource centralDir, + DataSource eocd, + List<SignerConfig> signerConfigs, + boolean v3SigningEnabled) + throws IOException, InvalidKeyException, NoSuchAlgorithmException, + SignatureException { + return generateApkSignatureSchemeV2Block(executor, beforeCentralDir, centralDir, eocd, + signerConfigs, v3SigningEnabled, null); + } + + public static ApkSigningBlockUtils.SigningSchemeBlockAndDigests + generateApkSignatureSchemeV2Block( + RunnablesExecutor executor, + DataSource beforeCentralDir, + DataSource centralDir, + DataSource eocd, + List<SignerConfig> signerConfigs, + boolean v3SigningEnabled, + List<byte[]> preservedV2SignerBlocks) + throws IOException, InvalidKeyException, NoSuchAlgorithmException, + SignatureException { + Pair<List<SignerConfig>, Map<ContentDigestAlgorithm, byte[]>> digestInfo = + ApkSigningBlockUtils.computeContentDigests( + executor, beforeCentralDir, centralDir, eocd, signerConfigs); + return new ApkSigningBlockUtils.SigningSchemeBlockAndDigests( + generateApkSignatureSchemeV2Block( + digestInfo.getFirst(), digestInfo.getSecond(), v3SigningEnabled, + preservedV2SignerBlocks), + digestInfo.getSecond()); + } + + private static Pair<byte[], Integer> generateApkSignatureSchemeV2Block( + List<SignerConfig> signerConfigs, + Map<ContentDigestAlgorithm, byte[]> contentDigests, + boolean v3SigningEnabled, + List<byte[]> preservedV2SignerBlocks) + throws NoSuchAlgorithmException, InvalidKeyException, SignatureException { + // FORMAT: + // * length-prefixed sequence of length-prefixed signer blocks. + + if (signerConfigs.size() > MAX_APK_SIGNERS) { + throw new IllegalArgumentException( + "APK Signature Scheme v2 only supports a maximum of " + MAX_APK_SIGNERS + ", " + + signerConfigs.size() + " provided"); + } + + List<byte[]> signerBlocks = new ArrayList<>(signerConfigs.size()); + if (preservedV2SignerBlocks != null && preservedV2SignerBlocks.size() > 0) { + signerBlocks.addAll(preservedV2SignerBlocks); + } + int signerNumber = 0; + for (SignerConfig signerConfig : signerConfigs) { + signerNumber++; + byte[] signerBlock; + try { + signerBlock = generateSignerBlock(signerConfig, contentDigests, v3SigningEnabled); + } catch (InvalidKeyException e) { + throw new InvalidKeyException("Signer #" + signerNumber + " failed", e); + } catch (SignatureException e) { + throw new SignatureException("Signer #" + signerNumber + " failed", e); + } + signerBlocks.add(signerBlock); + } + + return Pair.of( + encodeAsSequenceOfLengthPrefixedElements( + new byte[][] { + encodeAsSequenceOfLengthPrefixedElements(signerBlocks), + }), + V2SchemeConstants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID); + } + + private static byte[] generateSignerBlock( + SignerConfig signerConfig, + Map<ContentDigestAlgorithm, byte[]> contentDigests, + boolean v3SigningEnabled) + throws NoSuchAlgorithmException, InvalidKeyException, SignatureException { + if (signerConfig.certificates.isEmpty()) { + throw new SignatureException("No certificates configured for signer"); + } + PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey(); + + byte[] encodedPublicKey = encodePublicKey(publicKey); + + V2SignatureSchemeBlock.SignedData signedData = new V2SignatureSchemeBlock.SignedData(); + try { + signedData.certificates = encodeCertificates(signerConfig.certificates); + } catch (CertificateEncodingException e) { + throw new SignatureException("Failed to encode certificates", e); + } + + List<Pair<Integer, byte[]>> digests = + new ArrayList<>(signerConfig.signatureAlgorithms.size()); + for (SignatureAlgorithm signatureAlgorithm : signerConfig.signatureAlgorithms) { + ContentDigestAlgorithm contentDigestAlgorithm = + signatureAlgorithm.getContentDigestAlgorithm(); + byte[] contentDigest = contentDigests.get(contentDigestAlgorithm); + if (contentDigest == null) { + throw new RuntimeException( + contentDigestAlgorithm + + " content digest for " + + signatureAlgorithm + + " not computed"); + } + digests.add(Pair.of(signatureAlgorithm.getId(), contentDigest)); + } + signedData.digests = digests; + signedData.additionalAttributes = generateAdditionalAttributes(v3SigningEnabled); + + V2SignatureSchemeBlock.Signer signer = new V2SignatureSchemeBlock.Signer(); + // FORMAT: + // * length-prefixed sequence of length-prefixed digests: + // * uint32: signature algorithm ID + // * length-prefixed bytes: digest of contents + // * length-prefixed sequence of certificates: + // * length-prefixed bytes: X.509 certificate (ASN.1 DER encoded). + // * length-prefixed sequence of length-prefixed additional attributes: + // * uint32: ID + // * (length - 4) bytes: value + + signer.signedData = + encodeAsSequenceOfLengthPrefixedElements( + new byte[][] { + encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes( + signedData.digests), + encodeAsSequenceOfLengthPrefixedElements(signedData.certificates), + signedData.additionalAttributes, + new byte[0], + }); + signer.publicKey = encodedPublicKey; + signer.signatures = new ArrayList<>(); + signer.signatures = + ApkSigningBlockUtils.generateSignaturesOverData(signerConfig, signer.signedData); + + // FORMAT: + // * length-prefixed signed data + // * length-prefixed sequence of length-prefixed signatures: + // * uint32: signature algorithm ID + // * length-prefixed bytes: signature of signed data + // * length-prefixed bytes: public key (X.509 SubjectPublicKeyInfo, ASN.1 DER encoded) + return encodeAsSequenceOfLengthPrefixedElements( + new byte[][] { + signer.signedData, + encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes( + signer.signatures), + signer.publicKey, + }); + } + + private static byte[] generateAdditionalAttributes(boolean v3SigningEnabled) { + if (v3SigningEnabled) { + // FORMAT (little endian): + // * length-prefixed bytes: attribute pair + // * uint32: ID - STRIPPING_PROTECTION_ATTR_ID in this case + // * uint32: value - 3 (v3 signature scheme id) in this case + int payloadSize = 4 + 4 + 4; + ByteBuffer result = ByteBuffer.allocate(payloadSize); + result.order(ByteOrder.LITTLE_ENDIAN); + result.putInt(payloadSize - 4); + result.putInt(V2SchemeConstants.STRIPPING_PROTECTION_ATTR_ID); + result.putInt(ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3); + return result.array(); + } else { + return new byte[0]; + } + } + + private static final class V2SignatureSchemeBlock { + private static final class Signer { + public byte[] signedData; + public List<Pair<Integer, byte[]>> signatures; + public byte[] publicKey; + } + + private static final class SignedData { + public List<Pair<Integer, byte[]>> digests; + public List<byte[]> certificates; + public byte[] additionalAttributes; + } + } +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeVerifier.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeVerifier.java new file mode 100644 index 0000000000..4d6e3e1a8c --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeVerifier.java @@ -0,0 +1,471 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.apk.v2; + +import static com.android.apksig.Constants.MAX_APK_SIGNERS; + +import com.android.apksig.ApkVerifier.Issue; +import com.android.apksig.apk.ApkFormatException; +import com.android.apksig.apk.ApkUtils; +import com.android.apksig.internal.apk.ApkSigningBlockUtils; +import com.android.apksig.internal.apk.ContentDigestAlgorithm; +import com.android.apksig.internal.apk.SignatureAlgorithm; +import com.android.apksig.internal.apk.SignatureInfo; +import com.android.apksig.internal.util.ByteBufferUtils; +import com.android.apksig.internal.util.X509CertificateUtils; +import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate; +import com.android.apksig.util.DataSource; +import com.android.apksig.util.RunnablesExecutor; +import java.io.IOException; +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.Signature; +import java.security.SignatureException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.security.spec.AlgorithmParameterSpec; +import java.security.spec.X509EncodedKeySpec; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * APK Signature Scheme v2 verifier. + * + * <p>APK Signature Scheme v2 is a whole-file signature scheme which aims to protect every single + * bit of the APK, as opposed to the JAR Signature Scheme which protects only the names and + * uncompressed contents of ZIP entries. + * + * @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2</a> + */ +public abstract class V2SchemeVerifier { + /** Hidden constructor to prevent instantiation. */ + private V2SchemeVerifier() {} + + /** + * Verifies the provided APK's APK Signature Scheme v2 signatures and returns the result of + * verification. The APK must be considered verified only if + * {@link ApkSigningBlockUtils.Result#verified} is + * {@code true}. If verification fails, the result will contain errors -- see + * {@link ApkSigningBlockUtils.Result#getErrors()}. + * + * <p>Verification succeeds iff the APK's APK Signature Scheme v2 signatures are expected to + * verify on all Android platform versions in the {@code [minSdkVersion, maxSdkVersion]} range. + * If the APK's signature is expected to not verify on any of the specified platform versions, + * this method returns a result with one or more errors and whose + * {@code Result.verified == false}, or this method throws an exception. + * + * @throws ApkFormatException if the APK is malformed + * @throws NoSuchAlgorithmException if the APK's signatures cannot be verified because a + * required cryptographic algorithm implementation is missing + * @throws ApkSigningBlockUtils.SignatureNotFoundException if no APK Signature Scheme v2 + * signatures are found + * @throws IOException if an I/O error occurs when reading the APK + */ + public static ApkSigningBlockUtils.Result verify( + RunnablesExecutor executor, + DataSource apk, + ApkUtils.ZipSections zipSections, + Map<Integer, String> supportedApkSigSchemeNames, + Set<Integer> foundSigSchemeIds, + int minSdkVersion, + int maxSdkVersion) + throws IOException, ApkFormatException, NoSuchAlgorithmException, + ApkSigningBlockUtils.SignatureNotFoundException { + ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result( + ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2); + SignatureInfo signatureInfo = + ApkSigningBlockUtils.findSignature(apk, zipSections, + V2SchemeConstants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID , result); + + DataSource beforeApkSigningBlock = apk.slice(0, signatureInfo.apkSigningBlockOffset); + DataSource centralDir = + apk.slice( + signatureInfo.centralDirOffset, + signatureInfo.eocdOffset - signatureInfo.centralDirOffset); + ByteBuffer eocd = signatureInfo.eocd; + + verify(executor, + beforeApkSigningBlock, + signatureInfo.signatureBlock, + centralDir, + eocd, + supportedApkSigSchemeNames, + foundSigSchemeIds, + minSdkVersion, + maxSdkVersion, + result); + return result; + } + + /** + * Verifies the provided APK's v2 signatures and outputs the results into the provided + * {@code result}. APK is considered verified only if there are no errors reported in the + * {@code result}. See {@link #verify(RunnablesExecutor, DataSource, ApkUtils.ZipSections, Map, + * Set, int, int)} for more information about the contract of this method. + * + * @param result result populated by this method with interesting information about the APK, + * such as information about signers, and verification errors and warnings. + */ + private static void verify( + RunnablesExecutor executor, + DataSource beforeApkSigningBlock, + ByteBuffer apkSignatureSchemeV2Block, + DataSource centralDir, + ByteBuffer eocd, + Map<Integer, String> supportedApkSigSchemeNames, + Set<Integer> foundSigSchemeIds, + int minSdkVersion, + int maxSdkVersion, + ApkSigningBlockUtils.Result result) + throws IOException, NoSuchAlgorithmException { + Set<ContentDigestAlgorithm> contentDigestsToVerify = new HashSet<>(1); + parseSigners( + apkSignatureSchemeV2Block, + contentDigestsToVerify, + supportedApkSigSchemeNames, + foundSigSchemeIds, + minSdkVersion, + maxSdkVersion, + result); + if (result.containsErrors()) { + return; + } + ApkSigningBlockUtils.verifyIntegrity( + executor, beforeApkSigningBlock, centralDir, eocd, contentDigestsToVerify, result); + if (!result.containsErrors()) { + result.verified = true; + } + } + + /** + * Parses each signer in the provided APK Signature Scheme v2 block and populates corresponding + * {@code signerInfos} of the provided {@code result}. + * + * <p>This verifies signatures over {@code signed-data} block contained in each signer block. + * However, this does not verify the integrity of the rest of the APK but rather simply reports + * the expected digests of the rest of the APK (see {@code contentDigestsToVerify}). + * + * <p>This method adds one or more errors to the {@code result} if a verification error is + * expected to be encountered on an Android platform version in the + * {@code [minSdkVersion, maxSdkVersion]} range. + */ + public static void parseSigners( + ByteBuffer apkSignatureSchemeV2Block, + Set<ContentDigestAlgorithm> contentDigestsToVerify, + Map<Integer, String> supportedApkSigSchemeNames, + Set<Integer> foundApkSigSchemeIds, + int minSdkVersion, + int maxSdkVersion, + ApkSigningBlockUtils.Result result) throws NoSuchAlgorithmException { + ByteBuffer signers; + try { + signers = ApkSigningBlockUtils.getLengthPrefixedSlice(apkSignatureSchemeV2Block); + } catch (ApkFormatException e) { + result.addError(Issue.V2_SIG_MALFORMED_SIGNERS); + return; + } + if (!signers.hasRemaining()) { + result.addError(Issue.V2_SIG_NO_SIGNERS); + return; + } + + CertificateFactory certFactory; + try { + certFactory = CertificateFactory.getInstance("X.509"); + } catch (CertificateException e) { + throw new RuntimeException("Failed to obtain X.509 CertificateFactory", e); + } + int signerCount = 0; + while (signers.hasRemaining()) { + int signerIndex = signerCount; + signerCount++; + ApkSigningBlockUtils.Result.SignerInfo signerInfo = + new ApkSigningBlockUtils.Result.SignerInfo(); + signerInfo.index = signerIndex; + result.signers.add(signerInfo); + try { + ByteBuffer signer = ApkSigningBlockUtils.getLengthPrefixedSlice(signers); + parseSigner( + signer, + certFactory, + signerInfo, + contentDigestsToVerify, + supportedApkSigSchemeNames, + foundApkSigSchemeIds, + minSdkVersion, + maxSdkVersion); + } catch (ApkFormatException | BufferUnderflowException e) { + signerInfo.addError(Issue.V2_SIG_MALFORMED_SIGNER); + return; + } + } + if (signerCount > MAX_APK_SIGNERS) { + result.addError(Issue.V2_SIG_MAX_SIGNATURES_EXCEEDED, MAX_APK_SIGNERS, signerCount); + } + } + + /** + * Parses the provided signer block and populates the {@code result}. + * + * <p>This verifies signatures over {@code signed-data} contained in this block but does not + * verify the integrity of the rest of the APK. To facilitate APK integrity verification, this + * method adds the {@code contentDigestsToVerify}. These digests can then be used to verify the + * integrity of the APK. + * + * <p>This method adds one or more errors to the {@code result} if a verification error is + * expected to be encountered on an Android platform version in the + * {@code [minSdkVersion, maxSdkVersion]} range. + */ + private static void parseSigner( + ByteBuffer signerBlock, + CertificateFactory certFactory, + ApkSigningBlockUtils.Result.SignerInfo result, + Set<ContentDigestAlgorithm> contentDigestsToVerify, + Map<Integer, String> supportedApkSigSchemeNames, + Set<Integer> foundApkSigSchemeIds, + int minSdkVersion, + int maxSdkVersion) throws ApkFormatException, NoSuchAlgorithmException { + ByteBuffer signedData = ApkSigningBlockUtils.getLengthPrefixedSlice(signerBlock); + byte[] signedDataBytes = new byte[signedData.remaining()]; + signedData.get(signedDataBytes); + signedData.flip(); + result.signedData = signedDataBytes; + + ByteBuffer signatures = ApkSigningBlockUtils.getLengthPrefixedSlice(signerBlock); + byte[] publicKeyBytes = ApkSigningBlockUtils.readLengthPrefixedByteArray(signerBlock); + + // Parse the signatures block and identify supported signatures + int signatureCount = 0; + List<ApkSigningBlockUtils.SupportedSignature> supportedSignatures = new ArrayList<>(1); + while (signatures.hasRemaining()) { + signatureCount++; + try { + ByteBuffer signature = ApkSigningBlockUtils.getLengthPrefixedSlice(signatures); + int sigAlgorithmId = signature.getInt(); + byte[] sigBytes = ApkSigningBlockUtils.readLengthPrefixedByteArray(signature); + result.signatures.add( + new ApkSigningBlockUtils.Result.SignerInfo.Signature( + sigAlgorithmId, sigBytes)); + SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.findById(sigAlgorithmId); + if (signatureAlgorithm == null) { + result.addWarning(Issue.V2_SIG_UNKNOWN_SIG_ALGORITHM, sigAlgorithmId); + continue; + } + supportedSignatures.add( + new ApkSigningBlockUtils.SupportedSignature(signatureAlgorithm, sigBytes)); + } catch (ApkFormatException | BufferUnderflowException e) { + result.addError(Issue.V2_SIG_MALFORMED_SIGNATURE, signatureCount); + return; + } + } + if (result.signatures.isEmpty()) { + result.addError(Issue.V2_SIG_NO_SIGNATURES); + return; + } + + // Verify signatures over signed-data block using the public key + List<ApkSigningBlockUtils.SupportedSignature> signaturesToVerify = null; + try { + signaturesToVerify = + ApkSigningBlockUtils.getSignaturesToVerify( + supportedSignatures, minSdkVersion, maxSdkVersion); + } catch (ApkSigningBlockUtils.NoSupportedSignaturesException e) { + result.addError(Issue.V2_SIG_NO_SUPPORTED_SIGNATURES, e); + return; + } + for (ApkSigningBlockUtils.SupportedSignature signature : signaturesToVerify) { + SignatureAlgorithm signatureAlgorithm = signature.algorithm; + String jcaSignatureAlgorithm = + signatureAlgorithm.getJcaSignatureAlgorithmAndParams().getFirst(); + AlgorithmParameterSpec jcaSignatureAlgorithmParams = + signatureAlgorithm.getJcaSignatureAlgorithmAndParams().getSecond(); + String keyAlgorithm = signatureAlgorithm.getJcaKeyAlgorithm(); + PublicKey publicKey; + try { + publicKey = + KeyFactory.getInstance(keyAlgorithm).generatePublic( + new X509EncodedKeySpec(publicKeyBytes)); + } catch (Exception e) { + result.addError(Issue.V2_SIG_MALFORMED_PUBLIC_KEY, e); + return; + } + try { + Signature sig = Signature.getInstance(jcaSignatureAlgorithm); + sig.initVerify(publicKey); + if (jcaSignatureAlgorithmParams != null) { + sig.setParameter(jcaSignatureAlgorithmParams); + } + signedData.position(0); + sig.update(signedData); + byte[] sigBytes = signature.signature; + if (!sig.verify(sigBytes)) { + result.addError(Issue.V2_SIG_DID_NOT_VERIFY, signatureAlgorithm); + return; + } + result.verifiedSignatures.put(signatureAlgorithm, sigBytes); + contentDigestsToVerify.add(signatureAlgorithm.getContentDigestAlgorithm()); + } catch (InvalidKeyException | InvalidAlgorithmParameterException + | SignatureException e) { + result.addError(Issue.V2_SIG_VERIFY_EXCEPTION, signatureAlgorithm, e); + return; + } + } + + // At least one signature over signedData has verified. We can now parse signed-data. + signedData.position(0); + ByteBuffer digests = ApkSigningBlockUtils.getLengthPrefixedSlice(signedData); + ByteBuffer certificates = ApkSigningBlockUtils.getLengthPrefixedSlice(signedData); + ByteBuffer additionalAttributes = ApkSigningBlockUtils.getLengthPrefixedSlice(signedData); + + // Parse the certificates block + int certificateIndex = -1; + while (certificates.hasRemaining()) { + certificateIndex++; + byte[] encodedCert = ApkSigningBlockUtils.readLengthPrefixedByteArray(certificates); + X509Certificate certificate; + try { + certificate = X509CertificateUtils.generateCertificate(encodedCert, certFactory); + } catch (CertificateException e) { + result.addError( + Issue.V2_SIG_MALFORMED_CERTIFICATE, + certificateIndex, + certificateIndex + 1, + e); + return; + } + // Wrap the cert so that the result's getEncoded returns exactly the original encoded + // form. Without this, getEncoded may return a different form from what was stored in + // the signature. This is because some X509Certificate(Factory) implementations + // re-encode certificates. + certificate = new GuaranteedEncodedFormX509Certificate(certificate, encodedCert); + result.certs.add(certificate); + } + + if (result.certs.isEmpty()) { + result.addError(Issue.V2_SIG_NO_CERTIFICATES); + return; + } + X509Certificate mainCertificate = result.certs.get(0); + byte[] certificatePublicKeyBytes; + try { + certificatePublicKeyBytes = ApkSigningBlockUtils.encodePublicKey( + mainCertificate.getPublicKey()); + } catch (InvalidKeyException e) { + System.out.println("Caught an exception encoding the public key: " + e); + e.printStackTrace(); + certificatePublicKeyBytes = mainCertificate.getPublicKey().getEncoded(); + } + if (!Arrays.equals(publicKeyBytes, certificatePublicKeyBytes)) { + result.addError( + Issue.V2_SIG_PUBLIC_KEY_MISMATCH_BETWEEN_CERTIFICATE_AND_SIGNATURES_RECORD, + ApkSigningBlockUtils.toHex(certificatePublicKeyBytes), + ApkSigningBlockUtils.toHex(publicKeyBytes)); + return; + } + + // Parse the digests block + int digestCount = 0; + while (digests.hasRemaining()) { + digestCount++; + try { + ByteBuffer digest = ApkSigningBlockUtils.getLengthPrefixedSlice(digests); + int sigAlgorithmId = digest.getInt(); + byte[] digestBytes = ApkSigningBlockUtils.readLengthPrefixedByteArray(digest); + result.contentDigests.add( + new ApkSigningBlockUtils.Result.SignerInfo.ContentDigest( + sigAlgorithmId, digestBytes)); + } catch (ApkFormatException | BufferUnderflowException e) { + result.addError(Issue.V2_SIG_MALFORMED_DIGEST, digestCount); + return; + } + } + + List<Integer> sigAlgsFromSignaturesRecord = new ArrayList<>(result.signatures.size()); + for (ApkSigningBlockUtils.Result.SignerInfo.Signature signature : result.signatures) { + sigAlgsFromSignaturesRecord.add(signature.getAlgorithmId()); + } + List<Integer> sigAlgsFromDigestsRecord = new ArrayList<>(result.contentDigests.size()); + for (ApkSigningBlockUtils.Result.SignerInfo.ContentDigest digest : result.contentDigests) { + sigAlgsFromDigestsRecord.add(digest.getSignatureAlgorithmId()); + } + + if (!sigAlgsFromSignaturesRecord.equals(sigAlgsFromDigestsRecord)) { + result.addError( + Issue.V2_SIG_SIG_ALG_MISMATCH_BETWEEN_SIGNATURES_AND_DIGESTS_RECORDS, + sigAlgsFromSignaturesRecord, + sigAlgsFromDigestsRecord); + return; + } + + // Parse the additional attributes block. + int additionalAttributeCount = 0; + Set<Integer> supportedApkSigSchemeIds = supportedApkSigSchemeNames.keySet(); + Set<Integer> supportedExpectedApkSigSchemeIds = new HashSet<>(1); + while (additionalAttributes.hasRemaining()) { + additionalAttributeCount++; + try { + ByteBuffer attribute = + ApkSigningBlockUtils.getLengthPrefixedSlice(additionalAttributes); + int id = attribute.getInt(); + byte[] value = ByteBufferUtils.toByteArray(attribute); + result.additionalAttributes.add( + new ApkSigningBlockUtils.Result.SignerInfo.AdditionalAttribute(id, value)); + switch (id) { + case V2SchemeConstants.STRIPPING_PROTECTION_ATTR_ID: + // stripping protection added when signing with a newer scheme + int foundId = ByteBuffer.wrap(value).order( + ByteOrder.LITTLE_ENDIAN).getInt(); + if (supportedApkSigSchemeIds.contains(foundId)) { + supportedExpectedApkSigSchemeIds.add(foundId); + } else { + result.addWarning( + Issue.V2_SIG_UNKNOWN_APK_SIG_SCHEME_ID, result.index, foundId); + } + break; + default: + result.addWarning(Issue.V2_SIG_UNKNOWN_ADDITIONAL_ATTRIBUTE, id); + } + } catch (ApkFormatException | BufferUnderflowException e) { + result.addError( + Issue.V2_SIG_MALFORMED_ADDITIONAL_ATTRIBUTE, additionalAttributeCount); + return; + } + } + + // make sure that all known IDs indicated in stripping protection have already verified + for (int id : supportedExpectedApkSigSchemeIds) { + if (!foundApkSigSchemeIds.contains(id)) { + String apkSigSchemeName = supportedApkSigSchemeNames.get(id); + result.addError( + Issue.V2_SIG_MISSING_APK_SIG_REFERENCED, + result.index, + apkSigSchemeName); + } + } + } +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeConstants.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeConstants.java new file mode 100644 index 0000000000..dd92da344a --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeConstants.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.apk.v3; + +import com.android.apksig.internal.util.AndroidSdkVersion; + +/** Constants used by the V3 Signature Scheme signing and verification. */ +public class V3SchemeConstants { + private V3SchemeConstants() {} + + public static final int APK_SIGNATURE_SCHEME_V3_BLOCK_ID = 0xf05368c0; + public static final int APK_SIGNATURE_SCHEME_V31_BLOCK_ID = 0x1b93ad61; + public static final int PROOF_OF_ROTATION_ATTR_ID = 0x3ba06f8c; + + public static final int MIN_SDK_WITH_V3_SUPPORT = AndroidSdkVersion.P; + public static final int MIN_SDK_WITH_V31_SUPPORT = AndroidSdkVersion.T; + /** + * By default, APK signing key rotation will target T, but packages that have previously + * rotated can continue rotating on pre-T by specifying an SDK version <= 32 as the + * --rotation-min-sdk-version parameter when using apksigner or when invoking + * {@link com.android.apksig.ApkSigner.Builder#setMinSdkVersionForRotation(int)}. + */ + public static final int DEFAULT_ROTATION_MIN_SDK_VERSION = AndroidSdkVersion.T; + + /** + * This attribute is intended to be written to the V3.0 signer block as an additional attribute + * whose value is the minimum SDK version supported for rotation by the V3.1 signing block. If + * this value is set to X and a v3.1 signing block does not exist, or the minimum SDK version + * for rotation in the v3.1 signing block is not X, then the APK should be rejected. + */ + public static final int ROTATION_MIN_SDK_VERSION_ATTR_ID = 0x559f8b02; + + /** + * This attribute is written to the V3.1 signer block as an additional attribute to signify that + * the rotation-min-sdk-version is targeting a development release. This is required to support + * testing rotation on new development releases as the previous platform release SDK version + * is used as the development release SDK version until the development release SDK is + * finalized. + */ + public static final int ROTATION_ON_DEV_RELEASE_ATTR_ID = 0xc2a6b3ba; + + /** + * The current development release; rotation / signing configs targeting this release should + * be written with the {@link #PROD_RELEASE} SDK version and the dev release attribute. + */ + public static final int DEV_RELEASE = AndroidSdkVersion.U; + + /** + * The current production release. + */ + public static final int PROD_RELEASE = AndroidSdkVersion.T; +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeSigner.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeSigner.java new file mode 100644 index 0000000000..28f6589710 --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeSigner.java @@ -0,0 +1,531 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.apk.v3; + +import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsLengthPrefixedElement; +import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedElements; +import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes; +import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeCertificates; +import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodePublicKey; + +import com.android.apksig.SigningCertificateLineage; +import com.android.apksig.internal.apk.ApkSigningBlockUtils; +import com.android.apksig.internal.apk.ApkSigningBlockUtils.SigningSchemeBlockAndDigests; +import com.android.apksig.internal.apk.ApkSigningBlockUtils.SignerConfig; +import com.android.apksig.internal.apk.ContentDigestAlgorithm; +import com.android.apksig.internal.apk.SignatureAlgorithm; +import com.android.apksig.internal.util.Pair; +import com.android.apksig.util.DataSource; +import com.android.apksig.util.RunnablesExecutor; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.SignatureException; +import java.security.cert.CertificateEncodingException; +import java.security.interfaces.ECKey; +import java.security.interfaces.RSAKey; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.OptionalInt; + +/** + * APK Signature Scheme v3 signer. + * + * <p>APK Signature Scheme v3 builds upon APK Signature Scheme v3, and maintains all of the APK + * Signature Scheme v2 goals. + * + * @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2</a> + * <p>The main contribution of APK Signature Scheme v3 is the introduction of the {@link + * SigningCertificateLineage}, which enables an APK to change its signing certificate as long as + * it can prove the new siging certificate was signed by the old. + */ +public class V3SchemeSigner { + public static final int APK_SIGNATURE_SCHEME_V3_BLOCK_ID = + V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID; + public static final int PROOF_OF_ROTATION_ATTR_ID = V3SchemeConstants.PROOF_OF_ROTATION_ATTR_ID; + + private final RunnablesExecutor mExecutor; + private final DataSource mBeforeCentralDir; + private final DataSource mCentralDir; + private final DataSource mEocd; + private final List<SignerConfig> mSignerConfigs; + private final int mBlockId; + private final OptionalInt mOptionalV31MinSdkVersion; + private final boolean mRotationTargetsDevRelease; + + private V3SchemeSigner(DataSource beforeCentralDir, + DataSource centralDir, + DataSource eocd, + List<SignerConfig> signerConfigs, + RunnablesExecutor executor, + int blockId, + OptionalInt optionalV31MinSdkVersion, + boolean rotationTargetsDevRelease) { + mBeforeCentralDir = beforeCentralDir; + mCentralDir = centralDir; + mEocd = eocd; + mSignerConfigs = signerConfigs; + mExecutor = executor; + mBlockId = blockId; + mOptionalV31MinSdkVersion = optionalV31MinSdkVersion; + mRotationTargetsDevRelease = rotationTargetsDevRelease; + } + + /** + * Gets the APK Signature Scheme v3 signature algorithms to be used for signing an APK using the + * provided key. + * + * @param minSdkVersion minimum API Level of the platform on which the APK may be installed (see + * AndroidManifest.xml minSdkVersion attribute). + * @throws InvalidKeyException if the provided key is not suitable for signing APKs using APK + * Signature Scheme v3 + */ + public static List<SignatureAlgorithm> getSuggestedSignatureAlgorithms(PublicKey signingKey, + int minSdkVersion, boolean verityEnabled, boolean deterministicDsaSigning) + throws InvalidKeyException { + String keyAlgorithm = signingKey.getAlgorithm(); + if ("RSA".equalsIgnoreCase(keyAlgorithm)) { + // Use RSASSA-PKCS1-v1_5 signature scheme instead of RSASSA-PSS to guarantee + // deterministic signatures which make life easier for OTA updates (fewer files + // changed when deterministic signature schemes are used). + + // Pick a digest which is no weaker than the key. + int modulusLengthBits = ((RSAKey) signingKey).getModulus().bitLength(); + if (modulusLengthBits <= 3072) { + // 3072-bit RSA is roughly 128-bit strong, meaning SHA-256 is a good fit. + List<SignatureAlgorithm> algorithms = new ArrayList<>(); + algorithms.add(SignatureAlgorithm.RSA_PKCS1_V1_5_WITH_SHA256); + if (verityEnabled) { + algorithms.add(SignatureAlgorithm.VERITY_RSA_PKCS1_V1_5_WITH_SHA256); + } + return algorithms; + } else { + // Keys longer than 3072 bit need to be paired with a stronger digest to avoid the + // digest being the weak link. SHA-512 is the next strongest supported digest. + return Collections.singletonList(SignatureAlgorithm.RSA_PKCS1_V1_5_WITH_SHA512); + } + } else if ("DSA".equalsIgnoreCase(keyAlgorithm)) { + // DSA is supported only with SHA-256. + List<SignatureAlgorithm> algorithms = new ArrayList<>(); + algorithms.add( + deterministicDsaSigning ? + SignatureAlgorithm.DETDSA_WITH_SHA256 : + SignatureAlgorithm.DSA_WITH_SHA256); + if (verityEnabled) { + algorithms.add(SignatureAlgorithm.VERITY_DSA_WITH_SHA256); + } + return algorithms; + } else if ("EC".equalsIgnoreCase(keyAlgorithm)) { + // Pick a digest which is no weaker than the key. + int keySizeBits = ((ECKey) signingKey).getParams().getOrder().bitLength(); + if (keySizeBits <= 256) { + // 256-bit Elliptic Curve is roughly 128-bit strong, meaning SHA-256 is a good fit. + List<SignatureAlgorithm> algorithms = new ArrayList<>(); + algorithms.add(SignatureAlgorithm.ECDSA_WITH_SHA256); + if (verityEnabled) { + algorithms.add(SignatureAlgorithm.VERITY_ECDSA_WITH_SHA256); + } + return algorithms; + } else { + // Keys longer than 256 bit need to be paired with a stronger digest to avoid the + // digest being the weak link. SHA-512 is the next strongest supported digest. + return Collections.singletonList(SignatureAlgorithm.ECDSA_WITH_SHA512); + } + } else { + throw new InvalidKeyException("Unsupported key algorithm: " + keyAlgorithm); + } + } + + public static SigningSchemeBlockAndDigests generateApkSignatureSchemeV3Block( + RunnablesExecutor executor, + DataSource beforeCentralDir, + DataSource centralDir, + DataSource eocd, + List<SignerConfig> signerConfigs) + throws IOException, InvalidKeyException, NoSuchAlgorithmException, SignatureException { + return new V3SchemeSigner.Builder(beforeCentralDir, centralDir, eocd, signerConfigs) + .setRunnablesExecutor(executor) + .setBlockId(V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID) + .build() + .generateApkSignatureSchemeV3BlockAndDigests(); + } + + public static byte[] generateV3SignerAttribute( + SigningCertificateLineage signingCertificateLineage) { + // FORMAT (little endian): + // * length-prefixed bytes: attribute pair + // * uint32: ID + // * bytes: value - encoded V3 SigningCertificateLineage + byte[] encodedLineage = signingCertificateLineage.encodeSigningCertificateLineage(); + int payloadSize = 4 + 4 + encodedLineage.length; + ByteBuffer result = ByteBuffer.allocate(payloadSize); + result.order(ByteOrder.LITTLE_ENDIAN); + result.putInt(4 + encodedLineage.length); + result.putInt(V3SchemeConstants.PROOF_OF_ROTATION_ATTR_ID); + result.put(encodedLineage); + return result.array(); + } + + private static byte[] generateV3RotationMinSdkVersionStrippingProtectionAttribute( + int rotationMinSdkVersion) { + // FORMAT (little endian): + // * length-prefixed bytes: attribute pair + // * uint32: ID + // * bytes: value - int32 representing minimum SDK version for rotation + int payloadSize = 4 + 4 + 4; + ByteBuffer result = ByteBuffer.allocate(payloadSize); + result.order(ByteOrder.LITTLE_ENDIAN); + result.putInt(payloadSize - 4); + result.putInt(V3SchemeConstants.ROTATION_MIN_SDK_VERSION_ATTR_ID); + result.putInt(rotationMinSdkVersion); + return result.array(); + } + + private static byte[] generateV31RotationTargetsDevReleaseAttribute() { + // FORMAT (little endian): + // * length-prefixed bytes: attribute pair + // * uint32: ID + // * bytes: value - No value is used for this attribute + int payloadSize = 4 + 4; + ByteBuffer result = ByteBuffer.allocate(payloadSize); + result.order(ByteOrder.LITTLE_ENDIAN); + result.putInt(payloadSize - 4); + result.putInt(V3SchemeConstants.ROTATION_ON_DEV_RELEASE_ATTR_ID); + return result.array(); + } + + /** + * Generates and returns a new {@link SigningSchemeBlockAndDigests} containing the V3.x + * signing scheme block and digests based on the parameters provided to the {@link Builder}. + * + * @throws IOException if an I/O error occurs + * @throws NoSuchAlgorithmException if a required cryptographic algorithm implementation is + * missing + * @throws InvalidKeyException if the X.509 encoded form of the public key cannot be obtained + * @throws SignatureException if an error occurs when computing digests or generating + * signatures + */ + public SigningSchemeBlockAndDigests generateApkSignatureSchemeV3BlockAndDigests() + throws IOException, InvalidKeyException, NoSuchAlgorithmException, SignatureException { + Pair<List<SignerConfig>, Map<ContentDigestAlgorithm, byte[]>> digestInfo = + ApkSigningBlockUtils.computeContentDigests( + mExecutor, mBeforeCentralDir, mCentralDir, mEocd, mSignerConfigs); + return new SigningSchemeBlockAndDigests( + generateApkSignatureSchemeV3Block(digestInfo.getSecond()), digestInfo.getSecond()); + } + + private Pair<byte[], Integer> generateApkSignatureSchemeV3Block( + Map<ContentDigestAlgorithm, byte[]> contentDigests) + throws NoSuchAlgorithmException, InvalidKeyException, SignatureException { + // FORMAT: + // * length-prefixed sequence of length-prefixed signer blocks. + List<byte[]> signerBlocks = new ArrayList<>(mSignerConfigs.size()); + int signerNumber = 0; + for (SignerConfig signerConfig : mSignerConfigs) { + signerNumber++; + byte[] signerBlock; + try { + signerBlock = generateSignerBlock(signerConfig, contentDigests); + } catch (InvalidKeyException e) { + throw new InvalidKeyException("Signer #" + signerNumber + " failed", e); + } catch (SignatureException e) { + throw new SignatureException("Signer #" + signerNumber + " failed", e); + } + signerBlocks.add(signerBlock); + } + + return Pair.of( + encodeAsSequenceOfLengthPrefixedElements( + new byte[][] { + encodeAsSequenceOfLengthPrefixedElements(signerBlocks), + }), + mBlockId); + } + + private byte[] generateSignerBlock( + SignerConfig signerConfig, Map<ContentDigestAlgorithm, byte[]> contentDigests) + throws NoSuchAlgorithmException, InvalidKeyException, SignatureException { + if (signerConfig.certificates.isEmpty()) { + throw new SignatureException("No certificates configured for signer"); + } + PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey(); + + byte[] encodedPublicKey = encodePublicKey(publicKey); + + V3SignatureSchemeBlock.SignedData signedData = new V3SignatureSchemeBlock.SignedData(); + try { + signedData.certificates = encodeCertificates(signerConfig.certificates); + } catch (CertificateEncodingException e) { + throw new SignatureException("Failed to encode certificates", e); + } + + List<Pair<Integer, byte[]>> digests = + new ArrayList<>(signerConfig.signatureAlgorithms.size()); + for (SignatureAlgorithm signatureAlgorithm : signerConfig.signatureAlgorithms) { + ContentDigestAlgorithm contentDigestAlgorithm = + signatureAlgorithm.getContentDigestAlgorithm(); + byte[] contentDigest = contentDigests.get(contentDigestAlgorithm); + if (contentDigest == null) { + throw new RuntimeException( + contentDigestAlgorithm + + " content digest for " + + signatureAlgorithm + + " not computed"); + } + digests.add(Pair.of(signatureAlgorithm.getId(), contentDigest)); + } + signedData.digests = digests; + signedData.minSdkVersion = signerConfig.minSdkVersion; + signedData.maxSdkVersion = signerConfig.maxSdkVersion; + signedData.additionalAttributes = generateAdditionalAttributes(signerConfig); + + V3SignatureSchemeBlock.Signer signer = new V3SignatureSchemeBlock.Signer(); + + signer.signedData = encodeSignedData(signedData); + + signer.minSdkVersion = signerConfig.minSdkVersion; + signer.maxSdkVersion = signerConfig.maxSdkVersion; + signer.publicKey = encodedPublicKey; + signer.signatures = + ApkSigningBlockUtils.generateSignaturesOverData(signerConfig, signer.signedData); + + return encodeSigner(signer); + } + + private byte[] encodeSigner(V3SignatureSchemeBlock.Signer signer) { + byte[] signedData = encodeAsLengthPrefixedElement(signer.signedData); + byte[] signatures = + encodeAsLengthPrefixedElement( + encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes( + signer.signatures)); + byte[] publicKey = encodeAsLengthPrefixedElement(signer.publicKey); + + // FORMAT: + // * length-prefixed signed data + // * uint32: minSdkVersion + // * uint32: maxSdkVersion + // * length-prefixed sequence of length-prefixed signatures: + // * uint32: signature algorithm ID + // * length-prefixed bytes: signature of signed data + // * length-prefixed bytes: public key (X.509 SubjectPublicKeyInfo, ASN.1 DER encoded) + int payloadSize = signedData.length + 4 + 4 + signatures.length + publicKey.length; + + ByteBuffer result = ByteBuffer.allocate(payloadSize); + result.order(ByteOrder.LITTLE_ENDIAN); + result.put(signedData); + result.putInt(signer.minSdkVersion); + result.putInt(signer.maxSdkVersion); + result.put(signatures); + result.put(publicKey); + + return result.array(); + } + + private byte[] encodeSignedData(V3SignatureSchemeBlock.SignedData signedData) { + byte[] digests = + encodeAsLengthPrefixedElement( + encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes( + signedData.digests)); + byte[] certs = + encodeAsLengthPrefixedElement( + encodeAsSequenceOfLengthPrefixedElements(signedData.certificates)); + byte[] attributes = encodeAsLengthPrefixedElement(signedData.additionalAttributes); + + // FORMAT: + // * length-prefixed sequence of length-prefixed digests: + // * uint32: signature algorithm ID + // * length-prefixed bytes: digest of contents + // * length-prefixed sequence of certificates: + // * length-prefixed bytes: X.509 certificate (ASN.1 DER encoded). + // * uint-32: minSdkVersion + // * uint-32: maxSdkVersion + // * length-prefixed sequence of length-prefixed additional attributes: + // * uint32: ID + // * (length - 4) bytes: value + // * uint32: Proof-of-rotation ID: 0x3ba06f8c + // * length-prefixed roof-of-rotation structure + int payloadSize = digests.length + certs.length + 4 + 4 + attributes.length; + + ByteBuffer result = ByteBuffer.allocate(payloadSize); + result.order(ByteOrder.LITTLE_ENDIAN); + result.put(digests); + result.put(certs); + result.putInt(signedData.minSdkVersion); + result.putInt(signedData.maxSdkVersion); + result.put(attributes); + + return result.array(); + } + + private byte[] generateAdditionalAttributes(SignerConfig signerConfig) { + List<byte[]> attributes = new ArrayList<>(); + if (signerConfig.signingCertificateLineage != null) { + attributes.add(generateV3SignerAttribute(signerConfig.signingCertificateLineage)); + } + if ((mRotationTargetsDevRelease || signerConfig.signerTargetsDevRelease) + && mBlockId == V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID) { + attributes.add(generateV31RotationTargetsDevReleaseAttribute()); + } + if (mOptionalV31MinSdkVersion.isPresent() + && mBlockId == V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID) { + attributes.add(generateV3RotationMinSdkVersionStrippingProtectionAttribute( + mOptionalV31MinSdkVersion.getAsInt())); + } + int attributesSize = attributes.stream().mapToInt(attribute -> attribute.length).sum(); + byte[] attributesBuffer = new byte[attributesSize]; + if (attributesSize == 0) { + return new byte[0]; + } + int index = 0; + for (byte[] attribute : attributes) { + System.arraycopy(attribute, 0, attributesBuffer, index, attribute.length); + index += attribute.length; + } + return attributesBuffer; + } + + private static final class V3SignatureSchemeBlock { + private static final class Signer { + public byte[] signedData; + public int minSdkVersion; + public int maxSdkVersion; + public List<Pair<Integer, byte[]>> signatures; + public byte[] publicKey; + } + + private static final class SignedData { + public List<Pair<Integer, byte[]>> digests; + public List<byte[]> certificates; + public int minSdkVersion; + public int maxSdkVersion; + public byte[] additionalAttributes; + } + } + + /** Builder of {@link V3SchemeSigner} instances. */ + public static class Builder { + private final DataSource mBeforeCentralDir; + private final DataSource mCentralDir; + private final DataSource mEocd; + private final List<SignerConfig> mSignerConfigs; + + private RunnablesExecutor mExecutor = RunnablesExecutor.MULTI_THREADED; + private int mBlockId = V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID; + private OptionalInt mOptionalV31MinSdkVersion = OptionalInt.empty(); + private boolean mRotationTargetsDevRelease = false; + + /** + * Instantiates a new {@code Builder} with an APK's {@code beforeCentralDir}, {@code + * centralDir}, and {@code eocd}, along with a {@link List} of {@code signerConfigs} to + * be used to sign the APK. + */ + public Builder(DataSource beforeCentralDir, DataSource centralDir, DataSource eocd, + List<SignerConfig> signerConfigs) { + mBeforeCentralDir = beforeCentralDir; + mCentralDir = centralDir; + mEocd = eocd; + mSignerConfigs = signerConfigs; + } + + /** + * Sets the {@link RunnablesExecutor} to be used when computing the APK's content digests. + */ + public Builder setRunnablesExecutor(RunnablesExecutor executor) { + mExecutor = executor; + return this; + } + + /** + * Sets the {@code blockId} to be used for the V3 signature block. + * + * <p>This {@code V3SchemeSigner} currently supports the block IDs for the {@link + * V3SchemeConstants#APK_SIGNATURE_SCHEME_V3_BLOCK_ID v3.0} and {@link + * V3SchemeConstants#APK_SIGNATURE_SCHEME_V31_BLOCK_ID v3.1} signature schemes. + */ + public Builder setBlockId(int blockId) { + mBlockId = blockId; + return this; + } + + /** + * Sets the {@code rotationMinSdkVersion} to be written as an additional attribute in each + * signer's block. + * + * <p>This value provides stripping protection to ensure a v3.1 signing block with rotation + * is not modified or removed from the APK's signature block. + */ + public Builder setRotationMinSdkVersion(int rotationMinSdkVersion) { + return setMinSdkVersionForV31(rotationMinSdkVersion); + } + + /** + * Sets the {@code minSdkVersion} to be written as an additional attribute in each + * signer's block. + * + * <p>This value provides the stripping protection to ensure a v3.1 signing block is not + * modified or removed from the APK's signature block. + */ + public Builder setMinSdkVersionForV31(int minSdkVersion) { + if (minSdkVersion == V3SchemeConstants.DEV_RELEASE) { + minSdkVersion = V3SchemeConstants.PROD_RELEASE; + } + mOptionalV31MinSdkVersion = OptionalInt.of(minSdkVersion); + return this; + } + + /** + * Sets whether the minimum SDK version of a signer is intended to target a development + * release; this is primarily required after the T SDK is finalized, and an APK needs to + * target U during its development cycle for rotation. + * + * <p>This is only required after the T SDK is finalized since S and earlier releases do + * not know about the V3.1 block ID, but once T is released and work begins on U, U will + * use the SDK version of T during development. A signer with a minimum SDK version of T's + * SDK version along with setting {@code enabled} to true will allow an APK to use the + * rotated key on a device running U while causing this to be bypassed for T. + * + * <p><em>Note:</em>If the rotation-min-sdk-version is less than or equal to 32 (Android + * Sv2), then the rotated signing key will be used in the v3.0 signing block and this call + * will be a noop. + */ + public Builder setRotationTargetsDevRelease(boolean enabled) { + mRotationTargetsDevRelease = enabled; + return this; + } + + /** + * Returns a new {@link V3SchemeSigner} built with the configuration provided to this + * {@code Builder}. + */ + public V3SchemeSigner build() { + return new V3SchemeSigner(mBeforeCentralDir, + mCentralDir, + mEocd, + mSignerConfigs, + mExecutor, + mBlockId, + mOptionalV31MinSdkVersion, + mRotationTargetsDevRelease); + } + } +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeVerifier.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeVerifier.java new file mode 100644 index 0000000000..bd808f0e66 --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeVerifier.java @@ -0,0 +1,783 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.apk.v3; + +import static com.android.apksig.internal.apk.ApkSigningBlockUtils.getLengthPrefixedSlice; +import static com.android.apksig.internal.apk.ApkSigningBlockUtils.readLengthPrefixedByteArray; + +import com.android.apksig.ApkVerificationIssue; +import com.android.apksig.ApkVerifier.Issue; +import com.android.apksig.SigningCertificateLineage; +import com.android.apksig.apk.ApkFormatException; +import com.android.apksig.apk.ApkUtils; +import com.android.apksig.internal.apk.ApkSigningBlockUtils; +import com.android.apksig.internal.apk.ApkSigningBlockUtils.SignatureNotFoundException; +import com.android.apksig.internal.apk.ContentDigestAlgorithm; +import com.android.apksig.internal.apk.SignatureAlgorithm; +import com.android.apksig.internal.apk.SignatureInfo; +import com.android.apksig.internal.util.ByteBufferUtils; +import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate; +import com.android.apksig.internal.util.X509CertificateUtils; +import com.android.apksig.util.DataSource; +import com.android.apksig.util.RunnablesExecutor; + +import java.io.IOException; +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.Signature; +import java.security.SignatureException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.security.spec.AlgorithmParameterSpec; +import java.security.spec.X509EncodedKeySpec; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.OptionalInt; +import java.util.Set; +import java.util.SortedMap; +import java.util.TreeMap; + +/** + * APK Signature Scheme v3 verifier. + * + * <p>APK Signature Scheme v3, like v2 is a whole-file signature scheme which aims to protect every + * single bit of the APK, as opposed to the JAR Signature Scheme which protects only the names and + * uncompressed contents of ZIP entries. + * + * @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2</a> + */ +public class V3SchemeVerifier { + private final RunnablesExecutor mExecutor; + private final DataSource mApk; + private final ApkUtils.ZipSections mZipSections; + private final ApkSigningBlockUtils.Result mResult; + private final Set<ContentDigestAlgorithm> mContentDigestsToVerify; + private final int mMinSdkVersion; + private final int mMaxSdkVersion; + private final int mBlockId; + private final OptionalInt mOptionalRotationMinSdkVersion; + private final boolean mFullVerification; + + private ByteBuffer mApkSignatureSchemeV3Block; + + private V3SchemeVerifier( + RunnablesExecutor executor, + DataSource apk, + ApkUtils.ZipSections zipSections, + Set<ContentDigestAlgorithm> contentDigestsToVerify, + ApkSigningBlockUtils.Result result, + int minSdkVersion, + int maxSdkVersion, + int blockId, + OptionalInt optionalRotationMinSdkVersion, + boolean fullVerification) { + mExecutor = executor; + mApk = apk; + mZipSections = zipSections; + mContentDigestsToVerify = contentDigestsToVerify; + mResult = result; + mMinSdkVersion = minSdkVersion; + mMaxSdkVersion = maxSdkVersion; + mBlockId = blockId; + mOptionalRotationMinSdkVersion = optionalRotationMinSdkVersion; + mFullVerification = fullVerification; + } + + /** + * Verifies the provided APK's APK Signature Scheme v3 signatures and returns the result of + * verification. The APK must be considered verified only if + * {@link ApkSigningBlockUtils.Result#verified} is + * {@code true}. If verification fails, the result will contain errors -- see + * {@link ApkSigningBlockUtils.Result#getErrors()}. + * + * <p>Verification succeeds iff the APK's APK Signature Scheme v3 signatures are expected to + * verify on all Android platform versions in the {@code [minSdkVersion, maxSdkVersion]} range. + * If the APK's signature is expected to not verify on any of the specified platform versions, + * this method returns a result with one or more errors and whose + * {@code Result.verified == false}, or this method throws an exception. + * + * <p>This method only verifies the v3.0 signing block without platform targeted rotation from + * a v3.1 signing block. To verify a v3.1 signing block, or a v3.0 signing block in the presence + * of a v3.1 block, configure a new {@link V3SchemeVerifier} using the {@code Builder}. + * + * @throws NoSuchAlgorithmException if the APK's signatures cannot be verified because a + * required cryptographic algorithm implementation is missing + * @throws SignatureNotFoundException if no APK Signature Scheme v3 + * signatures are found + * @throws IOException if an I/O error occurs when reading the APK + */ + public static ApkSigningBlockUtils.Result verify( + RunnablesExecutor executor, + DataSource apk, + ApkUtils.ZipSections zipSections, + int minSdkVersion, + int maxSdkVersion) + throws IOException, NoSuchAlgorithmException, SignatureNotFoundException { + return new V3SchemeVerifier.Builder(apk, zipSections, minSdkVersion, maxSdkVersion) + .setRunnablesExecutor(executor) + .setBlockId(V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID) + .build() + .verify(); + } + + /** + * Verifies the provided APK's v3 signatures and outputs the results into the provided + * {@code result}. APK is considered verified only if there are no errors reported in the + * {@code result}. See {@link #verify(RunnablesExecutor, DataSource, ApkUtils.ZipSections, int, + * int)} for more information about the contract of this method. + * + * @return {@link ApkSigningBlockUtils.Result} populated with interesting information about the + * APK, such as information about signers, and verification errors and warnings + */ + public ApkSigningBlockUtils.Result verify() + throws IOException, NoSuchAlgorithmException, SignatureNotFoundException { + if (mApk == null || mZipSections == null) { + throw new IllegalStateException( + "A non-null apk and zip sections must be specified to verify an APK's v3 " + + "signatures"); + } + SignatureInfo signatureInfo = + ApkSigningBlockUtils.findSignature(mApk, mZipSections, mBlockId, mResult); + mApkSignatureSchemeV3Block = signatureInfo.signatureBlock; + + DataSource beforeApkSigningBlock = mApk.slice(0, signatureInfo.apkSigningBlockOffset); + DataSource centralDir = + mApk.slice( + signatureInfo.centralDirOffset, + signatureInfo.eocdOffset - signatureInfo.centralDirOffset); + ByteBuffer eocd = signatureInfo.eocd; + + parseSigners(); + + if (mResult.containsErrors()) { + return mResult; + } + ApkSigningBlockUtils.verifyIntegrity(mExecutor, beforeApkSigningBlock, centralDir, eocd, + mContentDigestsToVerify, mResult); + + // make sure that the v3 signers cover the entire targeted sdk version ranges and that the + // longest SigningCertificateHistory, if present, corresponds to the newest platform + // versions + SortedMap<Integer, ApkSigningBlockUtils.Result.SignerInfo> sortedSigners = new TreeMap<>(); + for (ApkSigningBlockUtils.Result.SignerInfo signer : mResult.signers) { + sortedSigners.put(signer.maxSdkVersion, signer); + } + + // first make sure there is neither overlap nor holes + int firstMin = 0; + int lastMax = 0; + int lastLineageSize = 0; + + // while we're iterating through the signers, build up the list of lineages + List<SigningCertificateLineage> lineages = new ArrayList<>(mResult.signers.size()); + + for (ApkSigningBlockUtils.Result.SignerInfo signer : sortedSigners.values()) { + int currentMin = signer.minSdkVersion; + int currentMax = signer.maxSdkVersion; + if (firstMin == 0) { + // first round sets up our basis + firstMin = currentMin; + } else { + // A signer's minimum SDK can equal the previous signer's maximum SDK if this signer + // is targeting a development release. + if (currentMin != (lastMax + 1) + && !(currentMin == lastMax && signerTargetsDevRelease(signer))) { + mResult.addError(Issue.V3_INCONSISTENT_SDK_VERSIONS); + break; + } + } + lastMax = currentMax; + + // also, while we're here, make sure that the lineage sizes only increase + if (signer.signingCertificateLineage != null) { + int currLineageSize = signer.signingCertificateLineage.size(); + if (currLineageSize < lastLineageSize) { + mResult.addError(Issue.V3_INCONSISTENT_LINEAGES); + break; + } + lastLineageSize = currLineageSize; + lineages.add(signer.signingCertificateLineage); + } + } + + // make sure we support our desired sdk ranges; if rotation is present in a v3.1 block + // then the max level only needs to support up to that sdk version for rotation. + if (firstMin > mMinSdkVersion + || lastMax < (mOptionalRotationMinSdkVersion.isPresent() + ? mOptionalRotationMinSdkVersion.getAsInt() - 1 : mMaxSdkVersion)) { + mResult.addError(Issue.V3_MISSING_SDK_VERSIONS, firstMin, lastMax); + } + + try { + mResult.signingCertificateLineage = + SigningCertificateLineage.consolidateLineages(lineages); + } catch (IllegalArgumentException e) { + mResult.addError(Issue.V3_INCONSISTENT_LINEAGES); + } + if (!mResult.containsErrors()) { + mResult.verified = true; + } + return mResult; + } + + /** + * Parses each signer in the provided APK Signature Scheme v3 block and populates corresponding + * {@code signerInfos} of the provided {@code result}. + * + * <p>This verifies signatures over {@code signed-data} block contained in each signer block. + * However, this does not verify the integrity of the rest of the APK but rather simply reports + * the expected digests of the rest of the APK (see {@code contentDigestsToVerify}). + * + * <p>This method adds one or more errors to the {@code result} if a verification error is + * expected to be encountered on an Android platform version in the + * {@code [minSdkVersion, maxSdkVersion]} range. + */ + public static void parseSigners( + ByteBuffer apkSignatureSchemeV3Block, + Set<ContentDigestAlgorithm> contentDigestsToVerify, + ApkSigningBlockUtils.Result result) throws NoSuchAlgorithmException { + try { + new V3SchemeVerifier.Builder(apkSignatureSchemeV3Block) + .setResult(result) + .setContentDigestsToVerify(contentDigestsToVerify) + .setFullVerification(false) + .build() + .parseSigners(); + } catch (IOException | SignatureNotFoundException e) { + // This should never occur since the apkSignatureSchemeV3Block was already provided. + throw new IllegalStateException("An exception was encountered when attempting to parse" + + " the signers from the provided APK Signature Scheme v3 block", e); + } + } + + /** + * Parses each signer in the APK Signature Scheme v3 block and populates corresponding + * {@link ApkSigningBlockUtils.Result.SignerInfo} instances in the + * returned {@link ApkSigningBlockUtils.Result}. + * + * <p>This verifies signatures over {@code signed-data} block contained in each signer block. + * However, this does not verify the integrity of the rest of the APK but rather simply reports + * the expected digests of the rest of the APK (see {@link Builder#setContentDigestsToVerify}). + * + * <p>This method adds one or more errors to the returned {@code Result} if a verification error + * is encountered when parsing the signers. + */ + public ApkSigningBlockUtils.Result parseSigners() + throws IOException, NoSuchAlgorithmException, SignatureNotFoundException { + ByteBuffer signers; + try { + if (mApkSignatureSchemeV3Block == null) { + SignatureInfo signatureInfo = + ApkSigningBlockUtils.findSignature(mApk, mZipSections, mBlockId, mResult); + mApkSignatureSchemeV3Block = signatureInfo.signatureBlock; + } + signers = getLengthPrefixedSlice(mApkSignatureSchemeV3Block); + } catch (ApkFormatException e) { + mResult.addError(Issue.V3_SIG_MALFORMED_SIGNERS); + return mResult; + } + if (!signers.hasRemaining()) { + mResult.addError(Issue.V3_SIG_NO_SIGNERS); + return mResult; + } + + CertificateFactory certFactory; + try { + certFactory = CertificateFactory.getInstance("X.509"); + } catch (CertificateException e) { + throw new RuntimeException("Failed to obtain X.509 CertificateFactory", e); + } + int signerCount = 0; + while (signers.hasRemaining()) { + int signerIndex = signerCount; + signerCount++; + ApkSigningBlockUtils.Result.SignerInfo signerInfo = + new ApkSigningBlockUtils.Result.SignerInfo(); + signerInfo.index = signerIndex; + mResult.signers.add(signerInfo); + try { + ByteBuffer signer = getLengthPrefixedSlice(signers); + parseSigner(signer, certFactory, signerInfo); + } catch (ApkFormatException | BufferUnderflowException e) { + signerInfo.addError(Issue.V3_SIG_MALFORMED_SIGNER); + return mResult; + } + } + return mResult; + } + + /** + * Parses the provided signer block and populates the {@code result}. + * + * <p>This verifies signatures over {@code signed-data} contained in this block, as well as + * the data contained therein, but does not verify the integrity of the rest of the APK. To + * facilitate APK integrity verification, this method adds the {@code contentDigestsToVerify}. + * These digests can then be used to verify the integrity of the APK. + * + * <p>This method adds one or more errors to the {@code result} if a verification error is + * expected to be encountered on an Android platform version in the + * {@code [minSdkVersion, maxSdkVersion]} range. + */ + private void parseSigner(ByteBuffer signerBlock, CertificateFactory certFactory, + ApkSigningBlockUtils.Result.SignerInfo result) + throws ApkFormatException, NoSuchAlgorithmException { + ByteBuffer signedData = getLengthPrefixedSlice(signerBlock); + byte[] signedDataBytes = new byte[signedData.remaining()]; + signedData.get(signedDataBytes); + signedData.flip(); + result.signedData = signedDataBytes; + + int parsedMinSdkVersion = signerBlock.getInt(); + int parsedMaxSdkVersion = signerBlock.getInt(); + result.minSdkVersion = parsedMinSdkVersion; + result.maxSdkVersion = parsedMaxSdkVersion; + if (parsedMinSdkVersion < 0 || parsedMinSdkVersion > parsedMaxSdkVersion) { + result.addError( + Issue.V3_SIG_INVALID_SDK_VERSIONS, parsedMinSdkVersion, parsedMaxSdkVersion); + } + ByteBuffer signatures = getLengthPrefixedSlice(signerBlock); + byte[] publicKeyBytes = readLengthPrefixedByteArray(signerBlock); + + // Parse the signatures block and identify supported signatures + int signatureCount = 0; + List<ApkSigningBlockUtils.SupportedSignature> supportedSignatures = new ArrayList<>(1); + while (signatures.hasRemaining()) { + signatureCount++; + try { + ByteBuffer signature = getLengthPrefixedSlice(signatures); + int sigAlgorithmId = signature.getInt(); + byte[] sigBytes = readLengthPrefixedByteArray(signature); + result.signatures.add( + new ApkSigningBlockUtils.Result.SignerInfo.Signature( + sigAlgorithmId, sigBytes)); + SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.findById(sigAlgorithmId); + if (signatureAlgorithm == null) { + result.addWarning(Issue.V3_SIG_UNKNOWN_SIG_ALGORITHM, sigAlgorithmId); + continue; + } + // TODO consider dropping deprecated signatures for v3 or modifying + // getSignaturesToVerify (called below) + supportedSignatures.add( + new ApkSigningBlockUtils.SupportedSignature(signatureAlgorithm, sigBytes)); + } catch (ApkFormatException | BufferUnderflowException e) { + result.addError(Issue.V3_SIG_MALFORMED_SIGNATURE, signatureCount); + return; + } + } + if (result.signatures.isEmpty()) { + result.addError(Issue.V3_SIG_NO_SIGNATURES); + return; + } + + // Verify signatures over signed-data block using the public key + List<ApkSigningBlockUtils.SupportedSignature> signaturesToVerify = null; + try { + signaturesToVerify = + ApkSigningBlockUtils.getSignaturesToVerify( + supportedSignatures, result.minSdkVersion, result.maxSdkVersion); + } catch (ApkSigningBlockUtils.NoSupportedSignaturesException e) { + result.addError(Issue.V3_SIG_NO_SUPPORTED_SIGNATURES); + return; + } + for (ApkSigningBlockUtils.SupportedSignature signature : signaturesToVerify) { + SignatureAlgorithm signatureAlgorithm = signature.algorithm; + String jcaSignatureAlgorithm = + signatureAlgorithm.getJcaSignatureAlgorithmAndParams().getFirst(); + AlgorithmParameterSpec jcaSignatureAlgorithmParams = + signatureAlgorithm.getJcaSignatureAlgorithmAndParams().getSecond(); + String keyAlgorithm = signatureAlgorithm.getJcaKeyAlgorithm(); + PublicKey publicKey; + try { + publicKey = + KeyFactory.getInstance(keyAlgorithm).generatePublic( + new X509EncodedKeySpec(publicKeyBytes)); + } catch (Exception e) { + result.addError(Issue.V3_SIG_MALFORMED_PUBLIC_KEY, e); + return; + } + try { + Signature sig = Signature.getInstance(jcaSignatureAlgorithm); + sig.initVerify(publicKey); + if (jcaSignatureAlgorithmParams != null) { + sig.setParameter(jcaSignatureAlgorithmParams); + } + signedData.position(0); + sig.update(signedData); + byte[] sigBytes = signature.signature; + if (!sig.verify(sigBytes)) { + result.addError(Issue.V3_SIG_DID_NOT_VERIFY, signatureAlgorithm); + return; + } + result.verifiedSignatures.put(signatureAlgorithm, sigBytes); + mContentDigestsToVerify.add(signatureAlgorithm.getContentDigestAlgorithm()); + } catch (InvalidKeyException | InvalidAlgorithmParameterException + | SignatureException e) { + result.addError(Issue.V3_SIG_VERIFY_EXCEPTION, signatureAlgorithm, e); + return; + } + } + + // At least one signature over signedData has verified. We can now parse signed-data. + signedData.position(0); + ByteBuffer digests = getLengthPrefixedSlice(signedData); + ByteBuffer certificates = getLengthPrefixedSlice(signedData); + + int signedMinSdkVersion = signedData.getInt(); + if (signedMinSdkVersion != parsedMinSdkVersion) { + result.addError( + Issue.V3_MIN_SDK_VERSION_MISMATCH_BETWEEN_SIGNER_AND_SIGNED_DATA_RECORD, + parsedMinSdkVersion, + signedMinSdkVersion); + } + int signedMaxSdkVersion = signedData.getInt(); + if (signedMaxSdkVersion != parsedMaxSdkVersion) { + result.addError( + Issue.V3_MAX_SDK_VERSION_MISMATCH_BETWEEN_SIGNER_AND_SIGNED_DATA_RECORD, + parsedMaxSdkVersion, + signedMaxSdkVersion); + } + ByteBuffer additionalAttributes = getLengthPrefixedSlice(signedData); + + // Parse the certificates block + int certificateIndex = -1; + while (certificates.hasRemaining()) { + certificateIndex++; + byte[] encodedCert = readLengthPrefixedByteArray(certificates); + X509Certificate certificate; + try { + certificate = X509CertificateUtils.generateCertificate(encodedCert, certFactory); + } catch (CertificateException e) { + result.addError( + Issue.V3_SIG_MALFORMED_CERTIFICATE, + certificateIndex, + certificateIndex + 1, + e); + return; + } + // Wrap the cert so that the result's getEncoded returns exactly the original encoded + // form. Without this, getEncoded may return a different form from what was stored in + // the signature. This is because some X509Certificate(Factory) implementations + // re-encode certificates. + certificate = new GuaranteedEncodedFormX509Certificate(certificate, encodedCert); + result.certs.add(certificate); + } + + if (result.certs.isEmpty()) { + result.addError(Issue.V3_SIG_NO_CERTIFICATES); + return; + } + X509Certificate mainCertificate = result.certs.get(0); + byte[] certificatePublicKeyBytes; + try { + certificatePublicKeyBytes = ApkSigningBlockUtils.encodePublicKey( + mainCertificate.getPublicKey()); + } catch (InvalidKeyException e) { + System.out.println("Caught an exception encoding the public key: " + e); + e.printStackTrace(); + certificatePublicKeyBytes = mainCertificate.getPublicKey().getEncoded(); + } + if (!Arrays.equals(publicKeyBytes, certificatePublicKeyBytes)) { + result.addError( + Issue.V3_SIG_PUBLIC_KEY_MISMATCH_BETWEEN_CERTIFICATE_AND_SIGNATURES_RECORD, + ApkSigningBlockUtils.toHex(certificatePublicKeyBytes), + ApkSigningBlockUtils.toHex(publicKeyBytes)); + return; + } + + // Parse the digests block + int digestCount = 0; + while (digests.hasRemaining()) { + digestCount++; + try { + ByteBuffer digest = getLengthPrefixedSlice(digests); + int sigAlgorithmId = digest.getInt(); + byte[] digestBytes = readLengthPrefixedByteArray(digest); + result.contentDigests.add( + new ApkSigningBlockUtils.Result.SignerInfo.ContentDigest( + sigAlgorithmId, digestBytes)); + } catch (ApkFormatException | BufferUnderflowException e) { + result.addError(Issue.V3_SIG_MALFORMED_DIGEST, digestCount); + return; + } + } + + List<Integer> sigAlgsFromSignaturesRecord = new ArrayList<>(result.signatures.size()); + for (ApkSigningBlockUtils.Result.SignerInfo.Signature signature : result.signatures) { + sigAlgsFromSignaturesRecord.add(signature.getAlgorithmId()); + } + List<Integer> sigAlgsFromDigestsRecord = new ArrayList<>(result.contentDigests.size()); + for (ApkSigningBlockUtils.Result.SignerInfo.ContentDigest digest : result.contentDigests) { + sigAlgsFromDigestsRecord.add(digest.getSignatureAlgorithmId()); + } + + if (!sigAlgsFromSignaturesRecord.equals(sigAlgsFromDigestsRecord)) { + result.addError( + Issue.V3_SIG_SIG_ALG_MISMATCH_BETWEEN_SIGNATURES_AND_DIGESTS_RECORDS, + sigAlgsFromSignaturesRecord, + sigAlgsFromDigestsRecord); + return; + } + + // Parse the additional attributes block. + int additionalAttributeCount = 0; + boolean rotationAttrFound = false; + while (additionalAttributes.hasRemaining()) { + additionalAttributeCount++; + try { + ByteBuffer attribute = + getLengthPrefixedSlice(additionalAttributes); + int id = attribute.getInt(); + byte[] value = ByteBufferUtils.toByteArray(attribute); + result.additionalAttributes.add( + new ApkSigningBlockUtils.Result.SignerInfo.AdditionalAttribute(id, value)); + if (id == V3SchemeConstants.PROOF_OF_ROTATION_ATTR_ID) { + try { + // SigningCertificateLineage is verified when built + result.signingCertificateLineage = + SigningCertificateLineage.readFromV3AttributeValue(value); + // make sure that the last cert in the chain matches this signer cert + SigningCertificateLineage subLineage = + result.signingCertificateLineage.getSubLineage(result.certs.get(0)); + if (result.signingCertificateLineage.size() != subLineage.size()) { + result.addError(Issue.V3_SIG_POR_CERT_MISMATCH); + } + } catch (SecurityException e) { + result.addError(Issue.V3_SIG_POR_DID_NOT_VERIFY); + } catch (IllegalArgumentException e) { + result.addError(Issue.V3_SIG_POR_CERT_MISMATCH); + } catch (Exception e) { + result.addError(Issue.V3_SIG_MALFORMED_LINEAGE); + } + } else if (id == V3SchemeConstants.ROTATION_MIN_SDK_VERSION_ATTR_ID) { + rotationAttrFound = true; + // API targeting for rotation was added with V3.1; if the maxSdkVersion + // does not support v3.1 then ignore this attribute. + if (mMaxSdkVersion >= V3SchemeConstants.MIN_SDK_WITH_V31_SUPPORT + && mFullVerification) { + int attrRotationMinSdkVersion = ByteBuffer.wrap(value) + .order(ByteOrder.LITTLE_ENDIAN).getInt(); + if (mOptionalRotationMinSdkVersion.isPresent()) { + int rotationMinSdkVersion = mOptionalRotationMinSdkVersion.getAsInt(); + if (attrRotationMinSdkVersion != rotationMinSdkVersion) { + result.addError(Issue.V31_ROTATION_MIN_SDK_MISMATCH, + attrRotationMinSdkVersion, rotationMinSdkVersion); + } + } else { + result.addError(Issue.V31_BLOCK_MISSING, attrRotationMinSdkVersion); + } + } + } else if (id == V3SchemeConstants.ROTATION_ON_DEV_RELEASE_ATTR_ID) { + // This attribute should only be used by a v3.1 signer to indicate rotation + // is targeting the development release that is using the SDK version of the + // previously released platform version. + if (mBlockId != V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID) { + result.addWarning(Issue.V31_ROTATION_TARGETS_DEV_RELEASE_ATTR_ON_V3_SIGNER); + } + } else { + result.addWarning(Issue.V3_SIG_UNKNOWN_ADDITIONAL_ATTRIBUTE, id); + } + } catch (ApkFormatException | BufferUnderflowException e) { + result.addError( + Issue.V3_SIG_MALFORMED_ADDITIONAL_ATTRIBUTE, additionalAttributeCount); + return; + } + } + if (mFullVerification && mOptionalRotationMinSdkVersion.isPresent() && !rotationAttrFound) { + result.addWarning(Issue.V31_ROTATION_MIN_SDK_ATTR_MISSING, + mOptionalRotationMinSdkVersion.getAsInt()); + } + } + + /** + * Returns whether the specified {@code signerInfo} is targeting a development release. + */ + public static boolean signerTargetsDevRelease( + ApkSigningBlockUtils.Result.SignerInfo signerInfo) { + boolean result = signerInfo.additionalAttributes.stream() + .mapToInt(attribute -> attribute.getId()) + .anyMatch(attrId -> attrId == V3SchemeConstants.ROTATION_ON_DEV_RELEASE_ATTR_ID); + return result; + } + + /** Builder of {@link V3SchemeVerifier} instances. */ + public static class Builder { + private RunnablesExecutor mExecutor = RunnablesExecutor.SINGLE_THREADED; + private DataSource mApk; + private ApkUtils.ZipSections mZipSections; + private ByteBuffer mApkSignatureSchemeV3Block; + private Set<ContentDigestAlgorithm> mContentDigestsToVerify; + private ApkSigningBlockUtils.Result mResult; + private int mMinSdkVersion; + private int mMaxSdkVersion; + private int mBlockId = V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID; + private boolean mFullVerification = true; + private OptionalInt mOptionalRotationMinSdkVersion = OptionalInt.empty(); + + /** + * Instantiates a new {@code Builder} for a {@code V3SchemeVerifier} that can be used to + * verify the V3 signing block of the provided {@code apk} with the specified {@code + * zipSections} over the range from {@code minSdkVersion} to {@code maxSdkVersion}. + */ + public Builder(DataSource apk, ApkUtils.ZipSections zipSections, int minSdkVersion, + int maxSdkVersion) { + mApk = apk; + mZipSections = zipSections; + mMinSdkVersion = minSdkVersion; + mMaxSdkVersion = maxSdkVersion; + } + + /** + * Instantiates a new {@code Builder} for a {@code V3SchemeVerifier} that can be used to + * parse the {@link ApkSigningBlockUtils.Result.SignerInfo} instances from the {@code + * apkSignatureSchemeV3Block}. + * + * <note>Full verification of the v3 signature is not possible when instantiating a new + * {@code V3SchemeVerifier} with this method.</note> + */ + public Builder(ByteBuffer apkSignatureSchemeV3Block) { + mApkSignatureSchemeV3Block = apkSignatureSchemeV3Block; + } + + /** + * Sets the {@link RunnablesExecutor} to be used when verifying the APK's content digests. + */ + public Builder setRunnablesExecutor(RunnablesExecutor executor) { + mExecutor = executor; + return this; + } + + /** + * Sets the V3 {code blockId} to be verified in the provided APK. + * + * <p>This {@code V3SchemeVerifier} currently supports the block IDs for the {@link + * V3SchemeConstants#APK_SIGNATURE_SCHEME_V3_BLOCK_ID v3.0} and {@link + * V3SchemeConstants#APK_SIGNATURE_SCHEME_V31_BLOCK_ID v3.1} signature schemes. + */ + public Builder setBlockId(int blockId) { + mBlockId = blockId; + return this; + } + + /** + * Sets the {@code rotationMinSdkVersion} to be verified in the v3.0 signer's additional + * attribute. + * + * <p>This value can be obtained from the signers returned when verifying the v3.1 signing + * block of an APK; in the case of multiple signers targeting different SDK versions in the + * v3.1 signing block, the minimum SDK version from all the signers should be used. + */ + public Builder setRotationMinSdkVersion(int rotationMinSdkVersion) { + mOptionalRotationMinSdkVersion = OptionalInt.of(rotationMinSdkVersion); + return this; + } + + /** + * Sets the {@code result} instance to be used when returning verification results. + * + * <p>This method can be used when the caller already has a {@link + * ApkSigningBlockUtils.Result} and wants to store the verification results in this + * instance. + */ + public Builder setResult(ApkSigningBlockUtils.Result result) { + mResult = result; + return this; + } + + /** + * Sets the instance to be used to store the {@code contentDigestsToVerify}. + * + * <p>This method can be used when the caller needs access to the {@code + * contentDigestsToVerify} computed by this {@code V3SchemeVerifier}. + */ + public Builder setContentDigestsToVerify( + Set<ContentDigestAlgorithm> contentDigestsToVerify) { + mContentDigestsToVerify = contentDigestsToVerify; + return this; + } + + /** + * Sets whether full verification should be performed by the {@code V3SchemeVerifier} built + * from this instance. + * + * <note>{@link #verify()} will always verify the content digests for the APK, but this + * allows verification of the rotation minimum SDK version stripping attribute to be skipped + * for scenarios where this value may not have been parsed from a V3.1 signing block (such + * as when only {@link #parseSigners()} will be invoked.</note> + */ + public Builder setFullVerification(boolean fullVerification) { + mFullVerification = fullVerification; + return this; + } + + /** + * Returns a new {@link V3SchemeVerifier} built with the configuration provided to this + * {@code Builder}. + */ + public V3SchemeVerifier build() { + int sigSchemeVersion; + switch (mBlockId) { + case V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID: + sigSchemeVersion = ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3; + mMinSdkVersion = Math.max(mMinSdkVersion, + V3SchemeConstants.MIN_SDK_WITH_V3_SUPPORT); + break; + case V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID: + sigSchemeVersion = ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V31; + // V3.1 supports targeting an SDK version later than that of the initial release + // in which it is supported; allow any range for V3.1 as long as V3.0 covers the + // rest of the range. + mMinSdkVersion = mMaxSdkVersion; + break; + default: + throw new IllegalArgumentException( + String.format("Unsupported APK Signature Scheme V3 block ID: 0x%08x", + mBlockId)); + } + if (mResult == null) { + mResult = new ApkSigningBlockUtils.Result(sigSchemeVersion); + } + if (mContentDigestsToVerify == null) { + mContentDigestsToVerify = new HashSet<>(1); + } + + V3SchemeVerifier verifier = new V3SchemeVerifier( + mExecutor, + mApk, + mZipSections, + mContentDigestsToVerify, + mResult, + mMinSdkVersion, + mMaxSdkVersion, + mBlockId, + mOptionalRotationMinSdkVersion, + mFullVerification); + if (mApkSignatureSchemeV3Block != null) { + verifier.mApkSignatureSchemeV3Block = mApkSignatureSchemeV3Block; + } + return verifier; + } + } +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v3/V3SigningCertificateLineage.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v3/V3SigningCertificateLineage.java new file mode 100644 index 0000000000..4ae7a5365d --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v3/V3SigningCertificateLineage.java @@ -0,0 +1,314 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.apk.v3; + +import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsLengthPrefixedElement; +import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedElements; +import static com.android.apksig.internal.apk.ApkSigningBlockUtils.getLengthPrefixedSlice; +import static com.android.apksig.internal.apk.ApkSigningBlockUtils.readLengthPrefixedByteArray; + +import com.android.apksig.apk.ApkFormatException; +import com.android.apksig.internal.apk.ApkSigningBlockUtils; +import com.android.apksig.internal.apk.SignatureAlgorithm; +import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate; +import com.android.apksig.internal.util.X509CertificateUtils; + +import java.io.IOException; +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.Signature; +import java.security.SignatureException; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.security.spec.AlgorithmParameterSpec; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; + +/** + * APK Signer Lineage. + * + * <p>The signer lineage contains a history of signing certificates with each ancestor attesting to + * the validity of its descendant. Each additional descendant represents a new identity that can be + * used to sign an APK, and each generation has accompanying attributes which represent how the + * APK would like to view the older signing certificates, specifically how they should be trusted in + * certain situations. + * + * <p> Its primary use is to enable APK Signing Certificate Rotation. The Android platform verifies + * the APK Signer Lineage, and if the current signing certificate for the APK is in the Signer + * Lineage, and the Lineage contains the certificate the platform associates with the APK, it will + * allow upgrades to the new certificate. + * + * @see <a href="https://source.android.com/security/apksigning/index.html">Application Signing</a> + */ +public class V3SigningCertificateLineage { + + private final static int FIRST_VERSION = 1; + private final static int CURRENT_VERSION = FIRST_VERSION; + + /** + * Deserializes the binary representation of an {@link V3SigningCertificateLineage}. Also + * verifies that the structure is well-formed, e.g. that the signature for each node is from its + * parent. + */ + public static List<SigningCertificateNode> readSigningCertificateLineage(ByteBuffer inputBytes) + throws IOException { + List<SigningCertificateNode> result = new ArrayList<>(); + int nodeCount = 0; + if (inputBytes == null || !inputBytes.hasRemaining()) { + return null; + } + + ApkSigningBlockUtils.checkByteOrderLittleEndian(inputBytes); + + // FORMAT (little endian): + // * uint32: version code + // * sequence of length-prefixed (uint32): nodes + // * length-prefixed bytes: signed data + // * length-prefixed bytes: certificate + // * uint32: signature algorithm id + // * uint32: flags + // * uint32: signature algorithm id (used by to sign next cert in lineage) + // * length-prefixed bytes: signature over above signed data + + X509Certificate lastCert = null; + int lastSigAlgorithmId = 0; + + try { + int version = inputBytes.getInt(); + if (version != CURRENT_VERSION) { + // we only have one version to worry about right now, so just check it + throw new IllegalArgumentException("Encoded SigningCertificateLineage has a version" + + " different than any of which we are aware"); + } + HashSet<X509Certificate> certHistorySet = new HashSet<>(); + while (inputBytes.hasRemaining()) { + nodeCount++; + ByteBuffer nodeBytes = getLengthPrefixedSlice(inputBytes); + ByteBuffer signedData = getLengthPrefixedSlice(nodeBytes); + int flags = nodeBytes.getInt(); + int sigAlgorithmId = nodeBytes.getInt(); + SignatureAlgorithm sigAlgorithm = SignatureAlgorithm.findById(lastSigAlgorithmId); + byte[] signature = readLengthPrefixedByteArray(nodeBytes); + + if (lastCert != null) { + // Use previous level cert to verify current level + String jcaSignatureAlgorithm = + sigAlgorithm.getJcaSignatureAlgorithmAndParams().getFirst(); + AlgorithmParameterSpec jcaSignatureAlgorithmParams = + sigAlgorithm.getJcaSignatureAlgorithmAndParams().getSecond(); + PublicKey publicKey = lastCert.getPublicKey(); + Signature sig = Signature.getInstance(jcaSignatureAlgorithm); + sig.initVerify(publicKey); + if (jcaSignatureAlgorithmParams != null) { + sig.setParameter(jcaSignatureAlgorithmParams); + } + sig.update(signedData); + if (!sig.verify(signature)) { + throw new SecurityException("Unable to verify signature of certificate #" + + nodeCount + " using " + jcaSignatureAlgorithm + " when verifying" + + " V3SigningCertificateLineage object"); + } + } + + signedData.rewind(); + byte[] encodedCert = readLengthPrefixedByteArray(signedData); + int signedSigAlgorithm = signedData.getInt(); + if (lastCert != null && lastSigAlgorithmId != signedSigAlgorithm) { + throw new SecurityException("Signing algorithm ID mismatch for certificate #" + + nodeBytes + " when verifying V3SigningCertificateLineage object"); + } + lastCert = X509CertificateUtils.generateCertificate(encodedCert); + lastCert = new GuaranteedEncodedFormX509Certificate(lastCert, encodedCert); + if (certHistorySet.contains(lastCert)) { + throw new SecurityException("Encountered duplicate entries in " + + "SigningCertificateLineage at certificate #" + nodeCount + ". All " + + "signing certificates should be unique"); + } + certHistorySet.add(lastCert); + lastSigAlgorithmId = sigAlgorithmId; + result.add(new SigningCertificateNode( + lastCert, SignatureAlgorithm.findById(signedSigAlgorithm), + SignatureAlgorithm.findById(sigAlgorithmId), signature, flags)); + } + } catch(ApkFormatException | BufferUnderflowException e){ + throw new IOException("Failed to parse V3SigningCertificateLineage object", e); + } catch(NoSuchAlgorithmException | InvalidKeyException + | InvalidAlgorithmParameterException | SignatureException e){ + throw new SecurityException( + "Failed to verify signature over signed data for certificate #" + nodeCount + + " when parsing V3SigningCertificateLineage object", e); + } catch(CertificateException e){ + throw new SecurityException("Failed to decode certificate #" + nodeCount + + " when parsing V3SigningCertificateLineage object", e); + } + return result; + } + + /** + * encode the in-memory representation of this {@code V3SigningCertificateLineage} + */ + public static byte[] encodeSigningCertificateLineage( + List<SigningCertificateNode> signingCertificateLineage) { + // FORMAT (little endian): + // * version code + // * sequence of length-prefixed (uint32): nodes + // * length-prefixed bytes: signed data + // * length-prefixed bytes: certificate + // * uint32: signature algorithm id + // * uint32: flags + // * uint32: signature algorithm id (used by to sign next cert in lineage) + + List<byte[]> nodes = new ArrayList<>(); + for (SigningCertificateNode node : signingCertificateLineage) { + nodes.add(encodeSigningCertificateNode(node)); + } + byte [] encodedSigningCertificateLineage = encodeAsSequenceOfLengthPrefixedElements(nodes); + + // add the version code (uint32) on top of the encoded nodes + int payloadSize = 4 + encodedSigningCertificateLineage.length; + ByteBuffer encodedWithVersion = ByteBuffer.allocate(payloadSize); + encodedWithVersion.order(ByteOrder.LITTLE_ENDIAN); + encodedWithVersion.putInt(CURRENT_VERSION); + encodedWithVersion.put(encodedSigningCertificateLineage); + return encodedWithVersion.array(); + } + + public static byte[] encodeSigningCertificateNode(SigningCertificateNode node) { + // FORMAT (little endian): + // * length-prefixed bytes: signed data + // * length-prefixed bytes: certificate + // * uint32: signature algorithm id + // * uint32: flags + // * uint32: signature algorithm id (used by to sign next cert in lineage) + // * length-prefixed bytes: signature over signed data + int parentSigAlgorithmId = 0; + if (node.parentSigAlgorithm != null) { + parentSigAlgorithmId = node.parentSigAlgorithm.getId(); + } + int sigAlgorithmId = 0; + if (node.sigAlgorithm != null) { + sigAlgorithmId = node.sigAlgorithm.getId(); + } + byte[] prefixedSignedData = encodeSignedData(node.signingCert, parentSigAlgorithmId); + byte[] prefixedSignature = encodeAsLengthPrefixedElement(node.signature); + int payloadSize = prefixedSignedData.length + 4 + 4 + prefixedSignature.length; + ByteBuffer result = ByteBuffer.allocate(payloadSize); + result.order(ByteOrder.LITTLE_ENDIAN); + result.put(prefixedSignedData); + result.putInt(node.flags); + result.putInt(sigAlgorithmId); + result.put(prefixedSignature); + return result.array(); + } + + public static byte[] encodeSignedData(X509Certificate certificate, int flags) { + try { + byte[] prefixedCertificate = encodeAsLengthPrefixedElement(certificate.getEncoded()); + int payloadSize = 4 + prefixedCertificate.length; + ByteBuffer result = ByteBuffer.allocate(payloadSize); + result.order(ByteOrder.LITTLE_ENDIAN); + result.put(prefixedCertificate); + result.putInt(flags); + return encodeAsLengthPrefixedElement(result.array()); + } catch (CertificateEncodingException e) { + throw new RuntimeException( + "Failed to encode V3SigningCertificateLineage certificate", e); + } + } + + /** + * Represents one signing certificate in the {@link V3SigningCertificateLineage}, which + * generally means it is/was used at some point to sign the same APK of the others in the + * lineage. + */ + public static class SigningCertificateNode { + + public SigningCertificateNode( + X509Certificate signingCert, + SignatureAlgorithm parentSigAlgorithm, + SignatureAlgorithm sigAlgorithm, + byte[] signature, + int flags) { + this.signingCert = signingCert; + this.parentSigAlgorithm = parentSigAlgorithm; + this.sigAlgorithm = sigAlgorithm; + this.signature = signature; + this.flags = flags; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof SigningCertificateNode)) return false; + + SigningCertificateNode that = (SigningCertificateNode) o; + if (!signingCert.equals(that.signingCert)) return false; + if (parentSigAlgorithm != that.parentSigAlgorithm) return false; + if (sigAlgorithm != that.sigAlgorithm) return false; + if (!Arrays.equals(signature, that.signature)) return false; + if (flags != that.flags) return false; + + // we made it + return true; + } + + @Override + public int hashCode() { + int result = Objects.hash(signingCert, parentSigAlgorithm, sigAlgorithm, flags); + result = 31 * result + Arrays.hashCode(signature); + return result; + } + + /** + * the signing cert for this node. This is part of the data signed by the parent node. + */ + public final X509Certificate signingCert; + + /** + * the algorithm used by the this node's parent to bless this data. Its ID value is part of + * the data signed by the parent node. {@code null} for first node. + */ + public final SignatureAlgorithm parentSigAlgorithm; + + /** + * the algorithm used by the this nodeto bless the next node's data. Its ID value is part + * of the signed data of the next node. {@code null} for the last node. + */ + public SignatureAlgorithm sigAlgorithm; + + /** + * signature over the signed data (above). The signature is from this node's parent + * signing certificate, which should correspond to the signing certificate used to sign an + * APK before rotating to this one, and is formed using {@code signatureAlgorithm}. + */ + public final byte[] signature; + + /** + * the flags detailing how the platform should treat this signing cert + */ + public int flags; + } +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeSigner.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeSigner.java new file mode 100644 index 0000000000..7bf952d82a --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeSigner.java @@ -0,0 +1,440 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.apk.v4; + +import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2; +import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3; +import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V31; +import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeCertificates; +import static com.android.apksig.internal.apk.v2.V2SchemeConstants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID; +import static com.android.apksig.internal.apk.v3.V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID; +import static com.android.apksig.internal.apk.v3.V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID; + +import com.android.apksig.apk.ApkUtils; +import com.android.apksig.internal.apk.ApkSigningBlockUtils; +import com.android.apksig.internal.apk.ContentDigestAlgorithm; +import com.android.apksig.internal.apk.SignatureAlgorithm; +import com.android.apksig.internal.apk.SignatureInfo; +import com.android.apksig.internal.apk.v2.V2SchemeVerifier; +import com.android.apksig.internal.apk.v3.V3SchemeSigner; +import com.android.apksig.internal.apk.v3.V3SchemeVerifier; +import com.android.apksig.internal.util.Pair; +import com.android.apksig.util.DataSource; +import com.android.apksig.zip.ZipFormatException; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.SignatureException; +import java.security.cert.CertificateEncodingException; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * APK Signature Scheme V4 signer. V4 scheme file contains 2 mandatory fields - used during + * installation. And optional verity tree - has to be present during session commit. + * <p> + * The fields: + * <p> + * 1. hashingInfo - verity root hash and hashing info, + * 2. signingInfo - certificate, public key and signature, + * For more details see V4Signature. + * </p> + * (optional) verityTree: integer size prepended bytes of the verity hash tree. + * <p> + */ +public abstract class V4SchemeSigner { + /** + * Hidden constructor to prevent instantiation. + */ + private V4SchemeSigner() { + } + + public static class SignerConfig { + final public ApkSigningBlockUtils.SignerConfig v4Config; + final public ApkSigningBlockUtils.SignerConfig v41Config; + + public SignerConfig(List<ApkSigningBlockUtils.SignerConfig> v4Configs, + List<ApkSigningBlockUtils.SignerConfig> v41Configs) throws InvalidKeyException { + if (v4Configs == null || v4Configs.size() != 1) { + throw new InvalidKeyException("Only accepting one signer config for V4 Signature."); + } + if (v41Configs != null && v41Configs.size() != 1) { + throw new InvalidKeyException("Only accepting one signer config for V4.1 Signature."); + } + this.v4Config = v4Configs.get(0); + this.v41Config = v41Configs != null ? v41Configs.get(0) : null; + } + } + + /** + * Based on a public key, return a signing algorithm that supports verity. + */ + public static List<SignatureAlgorithm> getSuggestedSignatureAlgorithms(PublicKey signingKey, + int minSdkVersion, boolean apkSigningBlockPaddingSupported, + boolean deterministicDsaSigning) + throws InvalidKeyException { + List<SignatureAlgorithm> algorithms = V3SchemeSigner.getSuggestedSignatureAlgorithms( + signingKey, minSdkVersion, + apkSigningBlockPaddingSupported, deterministicDsaSigning); + // Keeping only supported algorithms. + for (Iterator<SignatureAlgorithm> iter = algorithms.listIterator(); iter.hasNext(); ) { + final SignatureAlgorithm algorithm = iter.next(); + if (!isSupported(algorithm.getContentDigestAlgorithm(), false)) { + iter.remove(); + } + } + return algorithms; + } + + /** + * Compute hash tree and generate v4 signature for a given APK. Write the serialized data to + * output file. + */ + public static void generateV4Signature( + DataSource apkContent, SignerConfig signerConfig, File outputFile) + throws IOException, InvalidKeyException, NoSuchAlgorithmException { + Pair<V4Signature, byte[]> pair = generateV4Signature(apkContent, signerConfig); + try (final OutputStream output = new FileOutputStream(outputFile)) { + pair.getFirst().writeTo(output); + V4Signature.writeBytes(output, pair.getSecond()); + } catch (IOException e) { + outputFile.delete(); + throw e; + } + } + + /** Generate v4 signature and hash tree for a given APK. */ + public static Pair<V4Signature, byte[]> generateV4Signature( + DataSource apkContent, + SignerConfig signerConfig) + throws IOException, InvalidKeyException, NoSuchAlgorithmException { + // Salt has to stay empty for fs-verity compatibility. + final byte[] salt = null; + // Not used by apksigner. + final byte[] additionalData = null; + + final long fileSize = apkContent.size(); + + // Obtaining the strongest supported digest for each of the v2/v3/v3.1 blocks + // (CHUNKED_SHA256 or CHUNKED_SHA512). + final Map<Integer, byte[]> apkDigests = getApkDigests(apkContent); + + // Obtaining the merkle tree and the root hash in verity format. + ApkSigningBlockUtils.VerityTreeAndDigest verityContentDigestInfo = + ApkSigningBlockUtils.computeChunkVerityTreeAndDigest(apkContent); + + final ContentDigestAlgorithm verityContentDigestAlgorithm = + verityContentDigestInfo.contentDigestAlgorithm; + final byte[] rootHash = verityContentDigestInfo.rootHash; + final byte[] tree = verityContentDigestInfo.tree; + + final Pair<Integer, Byte> hashingAlgorithmBlockSizePair = convertToV4HashingInfo( + verityContentDigestAlgorithm); + final V4Signature.HashingInfo hashingInfo = new V4Signature.HashingInfo( + hashingAlgorithmBlockSizePair.getFirst(), hashingAlgorithmBlockSizePair.getSecond(), + salt, rootHash); + + // Generating SigningInfo and combining everything into V4Signature. + final V4Signature signature; + try { + signature = generateSignature(signerConfig, hashingInfo, apkDigests, additionalData, + fileSize); + } catch (InvalidKeyException | SignatureException | CertificateEncodingException e) { + throw new InvalidKeyException("Signer failed", e); + } + + return Pair.of(signature, tree); + } + + private static V4Signature.SigningInfo generateSigningInfo( + ApkSigningBlockUtils.SignerConfig signerConfig, + V4Signature.HashingInfo hashingInfo, + byte[] apkDigest, byte[] additionalData, long fileSize) + throws NoSuchAlgorithmException, InvalidKeyException, SignatureException, + CertificateEncodingException { + if (signerConfig.certificates.isEmpty()) { + throw new SignatureException("No certificates configured for signer"); + } + if (signerConfig.certificates.size() != 1) { + throw new CertificateEncodingException("Should only have one certificate"); + } + + // Collecting data for signing. + final PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey(); + + final List<byte[]> encodedCertificates = encodeCertificates(signerConfig.certificates); + final byte[] encodedCertificate = encodedCertificates.get(0); + + final V4Signature.SigningInfo signingInfoNoSignature = new V4Signature.SigningInfo(apkDigest, + encodedCertificate, additionalData, publicKey.getEncoded(), -1, null); + + final byte[] data = V4Signature.getSignedData(fileSize, hashingInfo, + signingInfoNoSignature); + + // Signing. + final List<Pair<Integer, byte[]>> signatures = + ApkSigningBlockUtils.generateSignaturesOverData(signerConfig, data); + if (signatures.size() != 1) { + throw new SignatureException("Should only be one signature generated"); + } + + final int signatureAlgorithmId = signatures.get(0).getFirst(); + final byte[] signature = signatures.get(0).getSecond(); + + return new V4Signature.SigningInfo(apkDigest, + encodedCertificate, additionalData, publicKey.getEncoded(), signatureAlgorithmId, + signature); + } + + private static V4Signature generateSignature( + SignerConfig signerConfig, + V4Signature.HashingInfo hashingInfo, + Map<Integer, byte[]> apkDigests, byte[] additionalData, long fileSize) + throws NoSuchAlgorithmException, InvalidKeyException, SignatureException, + CertificateEncodingException { + byte[] apkDigest = apkDigests.containsKey(VERSION_APK_SIGNATURE_SCHEME_V3) + ? apkDigests.get(VERSION_APK_SIGNATURE_SCHEME_V3) + : apkDigests.get(VERSION_APK_SIGNATURE_SCHEME_V2); + final V4Signature.SigningInfo signingInfo = generateSigningInfo(signerConfig.v4Config, + hashingInfo, apkDigest, additionalData, fileSize); + + final V4Signature.SigningInfos signingInfos; + if (signerConfig.v41Config != null) { + if (!apkDigests.containsKey(VERSION_APK_SIGNATURE_SCHEME_V31)) { + throw new IllegalStateException( + "V4.1 cannot be signed without a V3.1 content digest"); + } + apkDigest = apkDigests.get(VERSION_APK_SIGNATURE_SCHEME_V31); + final V4Signature.SigningInfoBlock extSigningBlock = new V4Signature.SigningInfoBlock( + APK_SIGNATURE_SCHEME_V31_BLOCK_ID, + generateSigningInfo(signerConfig.v41Config, hashingInfo, apkDigest, + additionalData, fileSize).toByteArray()); + signingInfos = new V4Signature.SigningInfos(signingInfo, extSigningBlock); + } else { + signingInfos = new V4Signature.SigningInfos(signingInfo); + } + + return new V4Signature(V4Signature.CURRENT_VERSION, hashingInfo.toByteArray(), + signingInfos.toByteArray()); + } + + /** + * Returns a {@code Map} from the APK signature scheme version to a {@code byte[]} of the + * strongest supported content digest found in that version's signature block for the V2, + * V3, and V3.1 signatures in the provided {@code apk}. + * + * <p>If a supported content digest algorithm is not found in any of the signature blocks, + * or if the APK is not signed by any of these signature schemes, then an {@code IOException} + * is thrown. + */ + private static Map<Integer, byte[]> getApkDigests(DataSource apk) throws IOException { + ApkUtils.ZipSections zipSections; + try { + zipSections = ApkUtils.findZipSections(apk); + } catch (ZipFormatException e) { + throw new IOException("Malformed APK: not a ZIP archive", e); + } + + Map<Integer, byte[]> sigSchemeToDigest = new HashMap<>(1); + try { + byte[] digest = getBestV3Digest(apk, zipSections, VERSION_APK_SIGNATURE_SCHEME_V31); + sigSchemeToDigest.put(VERSION_APK_SIGNATURE_SCHEME_V31, digest); + } catch (SignatureException expected) { + // It is expected to catch a SignatureException if the APK does not have a v3.1 + // signature. + } + + SignatureException v3Exception = null; + try { + byte[] digest = getBestV3Digest(apk, zipSections, VERSION_APK_SIGNATURE_SCHEME_V3); + sigSchemeToDigest.put(VERSION_APK_SIGNATURE_SCHEME_V3, digest); + } catch (SignatureException e) { + v3Exception = e; + } + + SignatureException v2Exception = null; + try { + byte[] digest = getBestV2Digest(apk, zipSections); + sigSchemeToDigest.put(VERSION_APK_SIGNATURE_SCHEME_V2, digest); + } catch (SignatureException e) { + v2Exception = e; + } + + if (sigSchemeToDigest.size() > 0) { + return sigSchemeToDigest; + } + + throw new IOException( + "Failed to obtain v2/v3 digest, v3 exception: " + v3Exception + ", v2 exception: " + + v2Exception); + } + + private static byte[] getBestV3Digest(DataSource apk, ApkUtils.ZipSections zipSections, + int v3SchemeVersion) throws SignatureException { + final Set<ContentDigestAlgorithm> contentDigestsToVerify = new HashSet<>(1); + final ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result( + v3SchemeVersion); + final int blockId; + switch (v3SchemeVersion) { + case VERSION_APK_SIGNATURE_SCHEME_V31: + blockId = APK_SIGNATURE_SCHEME_V31_BLOCK_ID; + break; + case VERSION_APK_SIGNATURE_SCHEME_V3: + blockId = APK_SIGNATURE_SCHEME_V3_BLOCK_ID; + break; + default: + throw new IllegalArgumentException( + "Invalid V3 scheme provided: " + v3SchemeVersion); + } + try { + final SignatureInfo signatureInfo = + ApkSigningBlockUtils.findSignature(apk, zipSections, blockId, result); + final ByteBuffer apkSignatureSchemeV3Block = signatureInfo.signatureBlock; + V3SchemeVerifier.parseSigners(apkSignatureSchemeV3Block, contentDigestsToVerify, + result); + } catch (Exception e) { + throw new SignatureException("Failed to extract and parse v3 block", e); + } + + if (result.signers.size() != 1) { + throw new SignatureException("Should only have one signer, errors: " + result.getErrors()); + } + + ApkSigningBlockUtils.Result.SignerInfo signer = result.signers.get(0); + if (signer.containsErrors()) { + throw new SignatureException("Parsing failed: " + signer.getErrors()); + } + + final List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> contentDigests = + result.signers.get(0).contentDigests; + return pickBestDigest(contentDigests); + } + + private static byte[] getBestV2Digest(DataSource apk, ApkUtils.ZipSections zipSections) + throws SignatureException { + final Set<ContentDigestAlgorithm> contentDigestsToVerify = new HashSet<>(1); + final Set<Integer> foundApkSigSchemeIds = new HashSet<>(1); + final ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result( + ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2); + try { + final SignatureInfo signatureInfo = + ApkSigningBlockUtils.findSignature(apk, zipSections, + APK_SIGNATURE_SCHEME_V2_BLOCK_ID, result); + final ByteBuffer apkSignatureSchemeV2Block = signatureInfo.signatureBlock; + V2SchemeVerifier.parseSigners( + apkSignatureSchemeV2Block, + contentDigestsToVerify, + Collections.emptyMap(), + foundApkSigSchemeIds, + Integer.MAX_VALUE, + Integer.MAX_VALUE, + result); + } catch (Exception e) { + throw new SignatureException("Failed to extract and parse v2 block", e); + } + + if (result.signers.size() != 1) { + throw new SignatureException("Should only have one signer, errors: " + result.getErrors()); + } + + ApkSigningBlockUtils.Result.SignerInfo signer = result.signers.get(0); + if (signer.containsErrors()) { + throw new SignatureException("Parsing failed: " + signer.getErrors()); + } + + final List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> contentDigests = + signer.contentDigests; + return pickBestDigest(contentDigests); + } + + private static byte[] pickBestDigest(List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> contentDigests) throws SignatureException { + if (contentDigests == null || contentDigests.isEmpty()) { + throw new SignatureException("Should have at least one digest"); + } + + int bestAlgorithmOrder = -1; + byte[] bestDigest = null; + for (ApkSigningBlockUtils.Result.SignerInfo.ContentDigest contentDigest : contentDigests) { + final SignatureAlgorithm signatureAlgorithm = + SignatureAlgorithm.findById(contentDigest.getSignatureAlgorithmId()); + final ContentDigestAlgorithm contentDigestAlgorithm = + signatureAlgorithm.getContentDigestAlgorithm(); + if (!isSupported(contentDigestAlgorithm, true)) { + continue; + } + final int algorithmOrder = digestAlgorithmSortingOrder(contentDigestAlgorithm); + if (bestAlgorithmOrder < algorithmOrder) { + bestAlgorithmOrder = algorithmOrder; + bestDigest = contentDigest.getValue(); + } + } + if (bestDigest == null) { + throw new SignatureException("Failed to find a supported digest in the source APK"); + } + return bestDigest; + } + + public static int digestAlgorithmSortingOrder(ContentDigestAlgorithm contentDigestAlgorithm) { + switch (contentDigestAlgorithm) { + case CHUNKED_SHA256: + return 0; + case VERITY_CHUNKED_SHA256: + return 1; + case CHUNKED_SHA512: + return 2; + default: + return -1; + } + } + + private static boolean isSupported(final ContentDigestAlgorithm contentDigestAlgorithm, + boolean forV3Digest) { + if (contentDigestAlgorithm == null) { + return false; + } + if (contentDigestAlgorithm == ContentDigestAlgorithm.CHUNKED_SHA256 + || contentDigestAlgorithm == ContentDigestAlgorithm.CHUNKED_SHA512 + || (forV3Digest + && contentDigestAlgorithm == ContentDigestAlgorithm.VERITY_CHUNKED_SHA256)) { + return true; + } + return false; + } + + private static Pair<Integer, Byte> convertToV4HashingInfo(ContentDigestAlgorithm algorithm) + throws NoSuchAlgorithmException { + switch (algorithm) { + case VERITY_CHUNKED_SHA256: + return Pair.of(V4Signature.HASHING_ALGORITHM_SHA256, + V4Signature.LOG2_BLOCK_SIZE_4096_BYTES); + default: + throw new NoSuchAlgorithmException( + "Invalid hash algorithm, only SHA2-256 over 4 KB chunks supported."); + } + } +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeVerifier.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeVerifier.java new file mode 100644 index 0000000000..c0a9013624 --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeVerifier.java @@ -0,0 +1,267 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.apk.v4; + +import static com.android.apksig.internal.apk.ApkSigningBlockUtils.toHex; + +import com.android.apksig.ApkVerifier; +import com.android.apksig.ApkVerifier.Issue; +import com.android.apksig.internal.apk.ApkSigningBlockUtils; +import com.android.apksig.internal.apk.ContentDigestAlgorithm; +import com.android.apksig.internal.apk.SignatureAlgorithm; +import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate; +import com.android.apksig.internal.util.X509CertificateUtils; +import com.android.apksig.util.DataSource; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.Signature; +import java.security.SignatureException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.security.spec.AlgorithmParameterSpec; +import java.security.spec.X509EncodedKeySpec; +import java.util.Arrays; + +/** + * APK Signature Scheme V4 verifier. + * <p> + * Verifies the serialized V4Signature file against an APK. + */ +public abstract class V4SchemeVerifier { + /** + * Hidden constructor to prevent instantiation. + */ + private V4SchemeVerifier() { + } + + /** + * <p> + * The main goals of the verifier are: 1) parse V4Signature file fields 2) verifies the PKCS7 + * signature block against the raw root hash bytes in the proto field 3) verifies that the raw + * root hash matches with the actual hash tree root of the give APK 4) if the file contains a + * verity tree, verifies that it matches with the actual verity tree computed from the given + * APK. + * </p> + */ + public static ApkSigningBlockUtils.Result verify(DataSource apk, File v4SignatureFile) + throws IOException, NoSuchAlgorithmException { + final V4Signature signature; + final byte[] tree; + try (InputStream input = new FileInputStream(v4SignatureFile)) { + signature = V4Signature.readFrom(input); + tree = V4Signature.readBytes(input); + } + + final ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result( + ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V4); + + if (signature == null) { + result.addError(Issue.V4_SIG_NO_SIGNATURES, + "Signature file does not contain a v4 signature."); + return result; + } + + if (signature.version != V4Signature.CURRENT_VERSION) { + result.addWarning(Issue.V4_SIG_VERSION_NOT_CURRENT, signature.version, + V4Signature.CURRENT_VERSION); + } + + V4Signature.HashingInfo hashingInfo = V4Signature.HashingInfo.fromByteArray( + signature.hashingInfo); + + V4Signature.SigningInfos signingInfos = V4Signature.SigningInfos.fromByteArray( + signature.signingInfos); + + final ApkSigningBlockUtils.Result.SignerInfo signerInfo; + + // Verify the primary signature over signedData. + { + V4Signature.SigningInfo signingInfo = signingInfos.signingInfo; + final byte[] signedData = V4Signature.getSignedData(apk.size(), hashingInfo, + signingInfo); + signerInfo = parseAndVerifySignatureBlock(signingInfo, signedData); + result.signers.add(signerInfo); + if (result.containsErrors()) { + return result; + } + } + + // Verify all subsequent signatures. + for (V4Signature.SigningInfoBlock signingInfoBlock : signingInfos.signingInfoBlocks) { + V4Signature.SigningInfo signingInfo = V4Signature.SigningInfo.fromByteArray( + signingInfoBlock.signingInfo); + final byte[] signedData = V4Signature.getSignedData(apk.size(), hashingInfo, + signingInfo); + result.signers.add(parseAndVerifySignatureBlock(signingInfo, signedData)); + if (result.containsErrors()) { + return result; + } + } + + // Check if the root hash and the tree are correct. + verifyRootHashAndTree(apk, signerInfo, hashingInfo.rawRootHash, tree); + if (!result.containsErrors()) { + result.verified = true; + } + + return result; + } + + /** + * Parses the provided signature block and populates the {@code result}. + * <p> + * This verifies {@signingInfo} over {@code signedData}, as well as parsing the certificate + * contained in the signature block. This method adds one or more errors to the {@code result}. + */ + private static ApkSigningBlockUtils.Result.SignerInfo parseAndVerifySignatureBlock( + V4Signature.SigningInfo signingInfo, + final byte[] signedData) throws NoSuchAlgorithmException { + final ApkSigningBlockUtils.Result.SignerInfo result = + new ApkSigningBlockUtils.Result.SignerInfo(); + result.index = 0; + + final int sigAlgorithmId = signingInfo.signatureAlgorithmId; + final byte[] sigBytes = signingInfo.signature; + result.signatures.add( + new ApkSigningBlockUtils.Result.SignerInfo.Signature(sigAlgorithmId, sigBytes)); + + SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.findById(sigAlgorithmId); + if (signatureAlgorithm == null) { + result.addError(Issue.V4_SIG_UNKNOWN_SIG_ALGORITHM, sigAlgorithmId); + return result; + } + + String jcaSignatureAlgorithm = + signatureAlgorithm.getJcaSignatureAlgorithmAndParams().getFirst(); + AlgorithmParameterSpec jcaSignatureAlgorithmParams = + signatureAlgorithm.getJcaSignatureAlgorithmAndParams().getSecond(); + + String keyAlgorithm = signatureAlgorithm.getJcaKeyAlgorithm(); + + final byte[] publicKeyBytes = signingInfo.publicKey; + PublicKey publicKey; + try { + publicKey = KeyFactory.getInstance(keyAlgorithm).generatePublic( + new X509EncodedKeySpec(publicKeyBytes)); + } catch (Exception e) { + result.addError(Issue.V4_SIG_MALFORMED_PUBLIC_KEY, e); + return result; + } + + try { + Signature sig = Signature.getInstance(jcaSignatureAlgorithm); + sig.initVerify(publicKey); + if (jcaSignatureAlgorithmParams != null) { + sig.setParameter(jcaSignatureAlgorithmParams); + } + sig.update(signedData); + if (!sig.verify(sigBytes)) { + result.addError(Issue.V4_SIG_DID_NOT_VERIFY, signatureAlgorithm); + return result; + } + result.verifiedSignatures.put(signatureAlgorithm, sigBytes); + } catch (InvalidKeyException | InvalidAlgorithmParameterException + | SignatureException e) { + result.addError(Issue.V4_SIG_VERIFY_EXCEPTION, signatureAlgorithm, e); + return result; + } + + if (signingInfo.certificate == null) { + result.addError(Issue.V4_SIG_NO_CERTIFICATE); + return result; + } + + final X509Certificate certificate; + try { + // Wrap the cert so that the result's getEncoded returns exactly the original encoded + // form. Without this, getEncoded may return a different form from what was stored in + // the signature. This is because some X509Certificate(Factory) implementations + // re-encode certificates. + certificate = new GuaranteedEncodedFormX509Certificate( + X509CertificateUtils.generateCertificate(signingInfo.certificate), + signingInfo.certificate); + } catch (CertificateException e) { + result.addError(Issue.V4_SIG_MALFORMED_CERTIFICATE, e); + return result; + } + result.certs.add(certificate); + + byte[] certificatePublicKeyBytes; + try { + certificatePublicKeyBytes = ApkSigningBlockUtils.encodePublicKey( + certificate.getPublicKey()); + } catch (InvalidKeyException e) { + System.out.println("Caught an exception encoding the public key: " + e); + e.printStackTrace(); + certificatePublicKeyBytes = certificate.getPublicKey().getEncoded(); + } + if (!Arrays.equals(publicKeyBytes, certificatePublicKeyBytes)) { + result.addError( + Issue.V4_SIG_PUBLIC_KEY_MISMATCH_BETWEEN_CERTIFICATE_AND_SIGNATURES_RECORD, + ApkSigningBlockUtils.toHex(certificatePublicKeyBytes), + ApkSigningBlockUtils.toHex(publicKeyBytes)); + return result; + } + + // Add apk digest from the file to the result. + ApkSigningBlockUtils.Result.SignerInfo.ContentDigest contentDigest = + new ApkSigningBlockUtils.Result.SignerInfo.ContentDigest( + 0 /* signature algorithm id doesn't matter here */, signingInfo.apkDigest); + result.contentDigests.add(contentDigest); + + return result; + } + + private static void verifyRootHashAndTree(DataSource apkContent, + ApkSigningBlockUtils.Result.SignerInfo signerInfo, byte[] expectedDigest, + byte[] expectedTree) throws IOException, NoSuchAlgorithmException { + ApkSigningBlockUtils.VerityTreeAndDigest actualContentDigestInfo = + ApkSigningBlockUtils.computeChunkVerityTreeAndDigest(apkContent); + + ContentDigestAlgorithm algorithm = actualContentDigestInfo.contentDigestAlgorithm; + final byte[] actualDigest = actualContentDigestInfo.rootHash; + final byte[] actualTree = actualContentDigestInfo.tree; + + if (!Arrays.equals(expectedDigest, actualDigest)) { + signerInfo.addError( + ApkVerifier.Issue.V4_SIG_APK_ROOT_DID_NOT_VERIFY, + algorithm, + toHex(expectedDigest), + toHex(actualDigest)); + return; + } + // Only check verity tree if it is not empty + if (expectedTree != null && !Arrays.equals(expectedTree, actualTree)) { + signerInfo.addError( + ApkVerifier.Issue.V4_SIG_APK_TREE_DID_NOT_VERIFY, + algorithm, + toHex(expectedDigest), + toHex(actualDigest)); + return; + } + + signerInfo.verifiedContentDigests.put(algorithm, actualDigest); + } +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v4/V4Signature.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v4/V4Signature.java new file mode 100644 index 0000000000..1eac5a2604 --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v4/V4Signature.java @@ -0,0 +1,311 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.apk.v4; + +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.Arrays; + +public class V4Signature { + public static final int CURRENT_VERSION = 2; + + public static final int HASHING_ALGORITHM_SHA256 = 1; + public static final byte LOG2_BLOCK_SIZE_4096_BYTES = 12; + + public static final int MAX_SIGNING_INFOS_SIZE = 7168; + + public static class HashingInfo { + public final int hashAlgorithm; // only 1 == SHA256 supported + public final byte log2BlockSize; // only 12 (block size 4096) supported now + public final byte[] salt; // used exactly as in fs-verity, 32 bytes max + public final byte[] rawRootHash; // salted digest of the first Merkle tree page + + HashingInfo(int hashAlgorithm, byte log2BlockSize, byte[] salt, byte[] rawRootHash) { + this.hashAlgorithm = hashAlgorithm; + this.log2BlockSize = log2BlockSize; + this.salt = salt; + this.rawRootHash = rawRootHash; + } + + static HashingInfo fromByteArray(byte[] bytes) throws IOException { + ByteBuffer buffer = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN); + final int hashAlgorithm = buffer.getInt(); + final byte log2BlockSize = buffer.get(); + byte[] salt = readBytes(buffer); + byte[] rawRootHash = readBytes(buffer); + return new HashingInfo(hashAlgorithm, log2BlockSize, salt, rawRootHash); + } + + byte[] toByteArray() { + final int size = 4/*hashAlgorithm*/ + 1/*log2BlockSize*/ + bytesSize(this.salt) + + bytesSize(this.rawRootHash); + ByteBuffer buffer = ByteBuffer.allocate(size).order(ByteOrder.LITTLE_ENDIAN); + buffer.putInt(this.hashAlgorithm); + buffer.put(this.log2BlockSize); + writeBytes(buffer, this.salt); + writeBytes(buffer, this.rawRootHash); + return buffer.array(); + } + } + + public static class SigningInfo { + public final byte[] apkDigest; // used to match with the corresponding APK + public final byte[] certificate; // ASN.1 DER form + public final byte[] additionalData; // a free-form binary data blob + public final byte[] publicKey; // ASN.1 DER, must match the certificate + public final int signatureAlgorithmId; // see the APK v2 doc for the list + public final byte[] signature; + + SigningInfo(byte[] apkDigest, byte[] certificate, byte[] additionalData, + byte[] publicKey, int signatureAlgorithmId, byte[] signature) { + this.apkDigest = apkDigest; + this.certificate = certificate; + this.additionalData = additionalData; + this.publicKey = publicKey; + this.signatureAlgorithmId = signatureAlgorithmId; + this.signature = signature; + } + + static SigningInfo fromByteArray(byte[] bytes) throws IOException { + return fromByteBuffer(ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN)); + } + + static SigningInfo fromByteBuffer(ByteBuffer buffer) throws IOException { + byte[] apkDigest = readBytes(buffer); + byte[] certificate = readBytes(buffer); + byte[] additionalData = readBytes(buffer); + byte[] publicKey = readBytes(buffer); + int signatureAlgorithmId = buffer.getInt(); + byte[] signature = readBytes(buffer); + return new SigningInfo(apkDigest, certificate, additionalData, publicKey, + signatureAlgorithmId, signature); + } + + byte[] toByteArray() { + final int size = bytesSize(this.apkDigest) + bytesSize(this.certificate) + bytesSize( + this.additionalData) + bytesSize(this.publicKey) + 4/*signatureAlgorithmId*/ + + bytesSize(this.signature); + ByteBuffer buffer = ByteBuffer.allocate(size).order(ByteOrder.LITTLE_ENDIAN); + writeBytes(buffer, this.apkDigest); + writeBytes(buffer, this.certificate); + writeBytes(buffer, this.additionalData); + writeBytes(buffer, this.publicKey); + buffer.putInt(this.signatureAlgorithmId); + writeBytes(buffer, this.signature); + return buffer.array(); + } + } + + public static class SigningInfoBlock { + public final int blockId; + public final byte[] signingInfo; + + public SigningInfoBlock(int blockId, byte[] signingInfo) { + this.blockId = blockId; + this.signingInfo = signingInfo; + } + + static SigningInfoBlock fromByteBuffer(ByteBuffer buffer) throws IOException { + int blockId = buffer.getInt(); + byte[] signingInfo = readBytes(buffer); + return new SigningInfoBlock(blockId, signingInfo); + } + + byte[] toByteArray() { + final int size = 4/*blockId*/ + bytesSize(this.signingInfo); + ByteBuffer buffer = ByteBuffer.allocate(size).order(ByteOrder.LITTLE_ENDIAN); + buffer.putInt(this.blockId); + writeBytes(buffer, this.signingInfo); + return buffer.array(); + } + } + + public static class SigningInfos { + public final SigningInfo signingInfo; + public final SigningInfoBlock[] signingInfoBlocks; + + public SigningInfos(SigningInfo signingInfo) { + this.signingInfo = signingInfo; + this.signingInfoBlocks = new SigningInfoBlock[0]; + } + + public SigningInfos(SigningInfo signingInfo, SigningInfoBlock... signingInfoBlocks) { + this.signingInfo = signingInfo; + this.signingInfoBlocks = signingInfoBlocks; + } + + public static SigningInfos fromByteArray(byte[] bytes) throws IOException { + ByteBuffer buffer = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN); + SigningInfo signingInfo = SigningInfo.fromByteBuffer(buffer); + if (!buffer.hasRemaining()) { + return new SigningInfos(signingInfo); + } + ArrayList<SigningInfoBlock> signingInfoBlocks = new ArrayList<>(1); + while (buffer.hasRemaining()) { + signingInfoBlocks.add(SigningInfoBlock.fromByteBuffer(buffer)); + } + return new SigningInfos(signingInfo, + signingInfoBlocks.toArray(new SigningInfoBlock[signingInfoBlocks.size()])); + } + + byte[] toByteArray() { + byte[][] arrays = new byte[1 + this.signingInfoBlocks.length][]; + arrays[0] = this.signingInfo.toByteArray(); + int size = arrays[0].length; + for (int i = 0, isize = this.signingInfoBlocks.length; i < isize; ++i) { + arrays[i + 1] = this.signingInfoBlocks[i].toByteArray(); + size += arrays[i + 1].length; + } + if (size > MAX_SIGNING_INFOS_SIZE) { + throw new IllegalArgumentException( + "Combined SigningInfos length exceeded limit of 7K: " + size); + } + + // Combine all arrays into one. + byte[] result = Arrays.copyOf(arrays[0], size); + int offset = arrays[0].length; + for (int i = 0, isize = this.signingInfoBlocks.length; i < isize; ++i) { + System.arraycopy(arrays[i + 1], 0, result, offset, arrays[i + 1].length); + offset += arrays[i + 1].length; + } + return result; + } + } + + // Always 2 for now. + public final int version; + public final byte[] hashingInfo; + // Can contain either SigningInfo or SigningInfo + one or multiple SigningInfoBlock. + // Passed as-is to the kernel. Can be retrieved later. + public final byte[] signingInfos; + + V4Signature(int version, byte[] hashingInfo, byte[] signingInfos) { + this.version = version; + this.hashingInfo = hashingInfo; + this.signingInfos = signingInfos; + } + + static V4Signature readFrom(InputStream stream) throws IOException { + final int version = readIntLE(stream); + if (version != CURRENT_VERSION) { + throw new IOException("Invalid signature version."); + } + final byte[] hashingInfo = readBytes(stream); + final byte[] signingInfo = readBytes(stream); + return new V4Signature(version, hashingInfo, signingInfo); + } + + public void writeTo(OutputStream stream) throws IOException { + writeIntLE(stream, this.version); + writeBytes(stream, this.hashingInfo); + writeBytes(stream, this.signingInfos); + } + + static byte[] getSignedData(long fileSize, HashingInfo hashingInfo, SigningInfo signingInfo) { + final int size = + 4/*size*/ + 8/*fileSize*/ + 4/*hash_algorithm*/ + 1/*log2_blocksize*/ + bytesSize( + hashingInfo.salt) + bytesSize(hashingInfo.rawRootHash) + bytesSize( + signingInfo.apkDigest) + bytesSize(signingInfo.certificate) + bytesSize( + signingInfo.additionalData); + ByteBuffer buffer = ByteBuffer.allocate(size).order(ByteOrder.LITTLE_ENDIAN); + buffer.putInt(size); + buffer.putLong(fileSize); + buffer.putInt(hashingInfo.hashAlgorithm); + buffer.put(hashingInfo.log2BlockSize); + writeBytes(buffer, hashingInfo.salt); + writeBytes(buffer, hashingInfo.rawRootHash); + writeBytes(buffer, signingInfo.apkDigest); + writeBytes(buffer, signingInfo.certificate); + writeBytes(buffer, signingInfo.additionalData); + return buffer.array(); + } + + // Utility methods. + static int bytesSize(byte[] bytes) { + return 4/*length*/ + (bytes == null ? 0 : bytes.length); + } + + static void readFully(InputStream stream, byte[] buffer) throws IOException { + int len = buffer.length; + int n = 0; + while (n < len) { + int count = stream.read(buffer, n, len - n); + if (count < 0) { + throw new EOFException(); + } + n += count; + } + } + + static int readIntLE(InputStream stream) throws IOException { + final byte[] buffer = new byte[4]; + readFully(stream, buffer); + return ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN).getInt(); + } + + static void writeIntLE(OutputStream stream, int v) throws IOException { + final byte[] buffer = ByteBuffer.wrap(new byte[4]).order(ByteOrder.LITTLE_ENDIAN).putInt(v).array(); + stream.write(buffer); + } + + static byte[] readBytes(InputStream stream) throws IOException { + try { + final int size = readIntLE(stream); + final byte[] bytes = new byte[size]; + readFully(stream, bytes); + return bytes; + } catch (EOFException ignored) { + return null; + } + } + + static byte[] readBytes(ByteBuffer buffer) throws IOException { + if (buffer.remaining() < 4) { + throw new EOFException(); + } + final int size = buffer.getInt(); + if (buffer.remaining() < size) { + throw new EOFException(); + } + final byte[] bytes = new byte[size]; + buffer.get(bytes); + return bytes; + } + + static void writeBytes(OutputStream stream, byte[] bytes) throws IOException { + if (bytes == null) { + writeIntLE(stream, 0); + return; + } + writeIntLE(stream, bytes.length); + stream.write(bytes); + } + + static void writeBytes(ByteBuffer buffer, byte[] bytes) { + if (bytes == null) { + buffer.putInt(0); + return; + } + buffer.putInt(bytes.length); + buffer.put(bytes); + } +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1BerParser.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1BerParser.java new file mode 100644 index 0000000000..160dc4e233 --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1BerParser.java @@ -0,0 +1,673 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.asn1; + +import com.android.apksig.internal.asn1.ber.BerDataValue; +import com.android.apksig.internal.asn1.ber.BerDataValueFormatException; +import com.android.apksig.internal.asn1.ber.BerDataValueReader; +import com.android.apksig.internal.asn1.ber.BerEncoding; +import com.android.apksig.internal.asn1.ber.ByteBufferBerDataValueReader; +import com.android.apksig.internal.util.ByteBufferUtils; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Parser of ASN.1 BER-encoded structures. + * + * <p>Structure is described to the parser by providing a class annotated with {@link Asn1Class}, + * containing fields annotated with {@link Asn1Field}. + */ +public final class Asn1BerParser { + private Asn1BerParser() {} + + /** + * Returns the ASN.1 structure contained in the BER encoded input. + * + * @param encoded encoded input. If the decoding operation succeeds, the position of this buffer + * is advanced to the first position following the end of the consumed structure. + * @param containerClass class describing the structure of the input. The class must meet the + * following requirements: + * <ul> + * <li>The class must be annotated with {@link Asn1Class}.</li> + * <li>The class must expose a public no-arg constructor.</li> + * <li>Member fields of the class which are populated with parsed input must be + * annotated with {@link Asn1Field} and be public and non-final.</li> + * </ul> + * + * @throws Asn1DecodingException if the input could not be decoded into the specified Java + * object + */ + public static <T> T parse(ByteBuffer encoded, Class<T> containerClass) + throws Asn1DecodingException { + BerDataValue containerDataValue; + try { + containerDataValue = new ByteBufferBerDataValueReader(encoded).readDataValue(); + } catch (BerDataValueFormatException e) { + throw new Asn1DecodingException("Failed to decode top-level data value", e); + } + if (containerDataValue == null) { + throw new Asn1DecodingException("Empty input"); + } + return parse(containerDataValue, containerClass); + } + + /** + * Returns the implicit {@code SET OF} contained in the provided ASN.1 BER input. Implicit means + * that this method does not care whether the tag number of this data structure is + * {@code SET OF} and whether the tag class is {@code UNIVERSAL}. + * + * <p>Note: The returned type is {@link List} rather than {@link java.util.Set} because ASN.1 + * SET may contain duplicate elements. + * + * @param encoded encoded input. If the decoding operation succeeds, the position of this buffer + * is advanced to the first position following the end of the consumed structure. + * @param elementClass class describing the structure of the values/elements contained in this + * container. The class must meet the following requirements: + * <ul> + * <li>The class must be annotated with {@link Asn1Class}.</li> + * <li>The class must expose a public no-arg constructor.</li> + * <li>Member fields of the class which are populated with parsed input must be + * annotated with {@link Asn1Field} and be public and non-final.</li> + * </ul> + * + * @throws Asn1DecodingException if the input could not be decoded into the specified Java + * object + */ + public static <T> List<T> parseImplicitSetOf(ByteBuffer encoded, Class<T> elementClass) + throws Asn1DecodingException { + BerDataValue containerDataValue; + try { + containerDataValue = new ByteBufferBerDataValueReader(encoded).readDataValue(); + } catch (BerDataValueFormatException e) { + throw new Asn1DecodingException("Failed to decode top-level data value", e); + } + if (containerDataValue == null) { + throw new Asn1DecodingException("Empty input"); + } + return parseSetOf(containerDataValue, elementClass); + } + + private static <T> T parse(BerDataValue container, Class<T> containerClass) + throws Asn1DecodingException { + if (container == null) { + throw new NullPointerException("container == null"); + } + if (containerClass == null) { + throw new NullPointerException("containerClass == null"); + } + + Asn1Type dataType = getContainerAsn1Type(containerClass); + switch (dataType) { + case CHOICE: + return parseChoice(container, containerClass); + + case SEQUENCE: + { + int expectedTagClass = BerEncoding.TAG_CLASS_UNIVERSAL; + int expectedTagNumber = BerEncoding.getTagNumber(dataType); + if ((container.getTagClass() != expectedTagClass) + || (container.getTagNumber() != expectedTagNumber)) { + throw new Asn1UnexpectedTagException( + "Unexpected data value read as " + containerClass.getName() + + ". Expected " + BerEncoding.tagClassAndNumberToString( + expectedTagClass, expectedTagNumber) + + ", but read: " + BerEncoding.tagClassAndNumberToString( + container.getTagClass(), container.getTagNumber())); + } + return parseSequence(container, containerClass); + } + case UNENCODED_CONTAINER: + return parseSequence(container, containerClass, true); + default: + throw new Asn1DecodingException("Parsing container " + dataType + " not supported"); + } + } + + private static <T> T parseChoice(BerDataValue dataValue, Class<T> containerClass) + throws Asn1DecodingException { + List<AnnotatedField> fields = getAnnotatedFields(containerClass); + if (fields.isEmpty()) { + throw new Asn1DecodingException( + "No fields annotated with " + Asn1Field.class.getName() + + " in CHOICE class " + containerClass.getName()); + } + + // Check that class + tagNumber don't clash between the choices + for (int i = 0; i < fields.size() - 1; i++) { + AnnotatedField f1 = fields.get(i); + int tagNumber1 = f1.getBerTagNumber(); + int tagClass1 = f1.getBerTagClass(); + for (int j = i + 1; j < fields.size(); j++) { + AnnotatedField f2 = fields.get(j); + int tagNumber2 = f2.getBerTagNumber(); + int tagClass2 = f2.getBerTagClass(); + if ((tagNumber1 == tagNumber2) && (tagClass1 == tagClass2)) { + throw new Asn1DecodingException( + "CHOICE fields are indistinguishable because they have the same tag" + + " class and number: " + containerClass.getName() + + "." + f1.getField().getName() + + " and ." + f2.getField().getName()); + } + } + } + + // Instantiate the container object / result + T obj; + try { + obj = containerClass.getConstructor().newInstance(); + } catch (IllegalArgumentException | ReflectiveOperationException e) { + throw new Asn1DecodingException("Failed to instantiate " + containerClass.getName(), e); + } + // Set the matching field's value from the data value + for (AnnotatedField field : fields) { + try { + field.setValueFrom(dataValue, obj); + return obj; + } catch (Asn1UnexpectedTagException expected) { + // not a match + } + } + + throw new Asn1DecodingException( + "No options of CHOICE " + containerClass.getName() + " matched"); + } + + private static <T> T parseSequence(BerDataValue container, Class<T> containerClass) + throws Asn1DecodingException { + return parseSequence(container, containerClass, false); + } + + private static <T> T parseSequence(BerDataValue container, Class<T> containerClass, + boolean isUnencodedContainer) throws Asn1DecodingException { + List<AnnotatedField> fields = getAnnotatedFields(containerClass); + Collections.sort( + fields, (f1, f2) -> f1.getAnnotation().index() - f2.getAnnotation().index()); + // Check that there are no fields with the same index + if (fields.size() > 1) { + AnnotatedField lastField = null; + for (AnnotatedField field : fields) { + if ((lastField != null) + && (lastField.getAnnotation().index() == field.getAnnotation().index())) { + throw new Asn1DecodingException( + "Fields have the same index: " + containerClass.getName() + + "." + lastField.getField().getName() + + " and ." + field.getField().getName()); + } + lastField = field; + } + } + + // Instantiate the container object / result + T t; + try { + t = containerClass.getConstructor().newInstance(); + } catch (IllegalArgumentException | ReflectiveOperationException e) { + throw new Asn1DecodingException("Failed to instantiate " + containerClass.getName(), e); + } + + // Parse fields one by one. A complication is that there may be optional fields. + int nextUnreadFieldIndex = 0; + BerDataValueReader elementsReader = container.contentsReader(); + while (nextUnreadFieldIndex < fields.size()) { + BerDataValue dataValue; + try { + // if this is the first field of an unencoded container then the entire contents of + // the container should be used when assigning to this field. + if (isUnencodedContainer && nextUnreadFieldIndex == 0) { + dataValue = container; + } else { + dataValue = elementsReader.readDataValue(); + } + } catch (BerDataValueFormatException e) { + throw new Asn1DecodingException("Malformed data value", e); + } + if (dataValue == null) { + break; + } + + for (int i = nextUnreadFieldIndex; i < fields.size(); i++) { + AnnotatedField field = fields.get(i); + try { + if (field.isOptional()) { + // Optional field -- might not be present and we may thus be trying to set + // it from the wrong tag. + try { + field.setValueFrom(dataValue, t); + nextUnreadFieldIndex = i + 1; + break; + } catch (Asn1UnexpectedTagException e) { + // This field is not present, attempt to use this data value for the + // next / iteration of the loop + continue; + } + } else { + // Mandatory field -- if we can't set its value from this data value, then + // it's an error + field.setValueFrom(dataValue, t); + nextUnreadFieldIndex = i + 1; + break; + } + } catch (Asn1DecodingException e) { + throw new Asn1DecodingException( + "Failed to parse " + containerClass.getName() + + "." + field.getField().getName(), + e); + } + } + } + + return t; + } + + // NOTE: This method returns List rather than Set because ASN.1 SET_OF does require uniqueness + // of elements -- it's an unordered collection. + @SuppressWarnings("unchecked") + private static <T> List<T> parseSetOf(BerDataValue container, Class<T> elementClass) + throws Asn1DecodingException { + List<T> result = new ArrayList<>(); + BerDataValueReader elementsReader = container.contentsReader(); + while (true) { + BerDataValue dataValue; + try { + dataValue = elementsReader.readDataValue(); + } catch (BerDataValueFormatException e) { + throw new Asn1DecodingException("Malformed data value", e); + } + if (dataValue == null) { + break; + } + T element; + if (ByteBuffer.class.equals(elementClass)) { + element = (T) dataValue.getEncodedContents(); + } else if (Asn1OpaqueObject.class.equals(elementClass)) { + element = (T) new Asn1OpaqueObject(dataValue.getEncoded()); + } else { + element = parse(dataValue, elementClass); + } + result.add(element); + } + return result; + } + + private static Asn1Type getContainerAsn1Type(Class<?> containerClass) + throws Asn1DecodingException { + Asn1Class containerAnnotation = containerClass.getDeclaredAnnotation(Asn1Class.class); + if (containerAnnotation == null) { + throw new Asn1DecodingException( + containerClass.getName() + " is not annotated with " + + Asn1Class.class.getName()); + } + + switch (containerAnnotation.type()) { + case CHOICE: + case SEQUENCE: + case UNENCODED_CONTAINER: + return containerAnnotation.type(); + default: + throw new Asn1DecodingException( + "Unsupported ASN.1 container annotation type: " + + containerAnnotation.type()); + } + } + + private static Class<?> getElementType(Field field) + throws Asn1DecodingException, ClassNotFoundException { + String type = field.getGenericType().getTypeName(); + int delimiterIndex = type.indexOf('<'); + if (delimiterIndex == -1) { + throw new Asn1DecodingException("Not a container type: " + field.getGenericType()); + } + int startIndex = delimiterIndex + 1; + int endIndex = type.indexOf('>', startIndex); + // TODO: handle comma? + if (endIndex == -1) { + throw new Asn1DecodingException("Not a container type: " + field.getGenericType()); + } + String elementClassName = type.substring(startIndex, endIndex); + return Class.forName(elementClassName); + } + + private static final class AnnotatedField { + private final Field mField; + private final Asn1Field mAnnotation; + private final Asn1Type mDataType; + private final Asn1TagClass mTagClass; + private final int mBerTagClass; + private final int mBerTagNumber; + private final Asn1Tagging mTagging; + private final boolean mOptional; + + public AnnotatedField(Field field, Asn1Field annotation) throws Asn1DecodingException { + mField = field; + mAnnotation = annotation; + mDataType = annotation.type(); + + Asn1TagClass tagClass = annotation.cls(); + if (tagClass == Asn1TagClass.AUTOMATIC) { + if (annotation.tagNumber() != -1) { + tagClass = Asn1TagClass.CONTEXT_SPECIFIC; + } else { + tagClass = Asn1TagClass.UNIVERSAL; + } + } + mTagClass = tagClass; + mBerTagClass = BerEncoding.getTagClass(mTagClass); + + int tagNumber; + if (annotation.tagNumber() != -1) { + tagNumber = annotation.tagNumber(); + } else if ((mDataType == Asn1Type.CHOICE) || (mDataType == Asn1Type.ANY)) { + tagNumber = -1; + } else { + tagNumber = BerEncoding.getTagNumber(mDataType); + } + mBerTagNumber = tagNumber; + + mTagging = annotation.tagging(); + if (((mTagging == Asn1Tagging.EXPLICIT) || (mTagging == Asn1Tagging.IMPLICIT)) + && (annotation.tagNumber() == -1)) { + throw new Asn1DecodingException( + "Tag number must be specified when tagging mode is " + mTagging); + } + + mOptional = annotation.optional(); + } + + public Field getField() { + return mField; + } + + public Asn1Field getAnnotation() { + return mAnnotation; + } + + public boolean isOptional() { + return mOptional; + } + + public int getBerTagClass() { + return mBerTagClass; + } + + public int getBerTagNumber() { + return mBerTagNumber; + } + + public void setValueFrom(BerDataValue dataValue, Object obj) throws Asn1DecodingException { + int readTagClass = dataValue.getTagClass(); + if (mBerTagNumber != -1) { + int readTagNumber = dataValue.getTagNumber(); + if ((readTagClass != mBerTagClass) || (readTagNumber != mBerTagNumber)) { + throw new Asn1UnexpectedTagException( + "Tag mismatch. Expected: " + + BerEncoding.tagClassAndNumberToString(mBerTagClass, mBerTagNumber) + + ", but found " + + BerEncoding.tagClassAndNumberToString(readTagClass, readTagNumber)); + } + } else { + if (readTagClass != mBerTagClass) { + throw new Asn1UnexpectedTagException( + "Tag mismatch. Expected class: " + + BerEncoding.tagClassToString(mBerTagClass) + + ", but found " + + BerEncoding.tagClassToString(readTagClass)); + } + } + + if (mTagging == Asn1Tagging.EXPLICIT) { + try { + dataValue = dataValue.contentsReader().readDataValue(); + } catch (BerDataValueFormatException e) { + throw new Asn1DecodingException( + "Failed to read contents of EXPLICIT data value", e); + } + } + + BerToJavaConverter.setFieldValue(obj, mField, mDataType, dataValue); + } + } + + private static class Asn1UnexpectedTagException extends Asn1DecodingException { + private static final long serialVersionUID = 1L; + + public Asn1UnexpectedTagException(String message) { + super(message); + } + } + + private static String oidToString(ByteBuffer encodedOid) throws Asn1DecodingException { + if (!encodedOid.hasRemaining()) { + throw new Asn1DecodingException("Empty OBJECT IDENTIFIER"); + } + + // First component encodes the first two nodes, X.Y, as X * 40 + Y, with 0 <= X <= 2 + long firstComponent = decodeBase128UnsignedLong(encodedOid); + int firstNode = (int) Math.min(firstComponent / 40, 2); + long secondNode = firstComponent - firstNode * 40; + StringBuilder result = new StringBuilder(); + result.append(Long.toString(firstNode)).append('.') + .append(Long.toString(secondNode)); + + // Each consecutive node is encoded as a separate component + while (encodedOid.hasRemaining()) { + long node = decodeBase128UnsignedLong(encodedOid); + result.append('.').append(Long.toString(node)); + } + + return result.toString(); + } + + private static long decodeBase128UnsignedLong(ByteBuffer encoded) throws Asn1DecodingException { + if (!encoded.hasRemaining()) { + return 0; + } + long result = 0; + while (encoded.hasRemaining()) { + if (result > Long.MAX_VALUE >>> 7) { + throw new Asn1DecodingException("Base-128 number too large"); + } + int b = encoded.get() & 0xff; + result <<= 7; + result |= b & 0x7f; + if ((b & 0x80) == 0) { + return result; + } + } + throw new Asn1DecodingException( + "Truncated base-128 encoded input: missing terminating byte, with highest bit not" + + " set"); + } + + private static BigInteger integerToBigInteger(ByteBuffer encoded) { + if (!encoded.hasRemaining()) { + return BigInteger.ZERO; + } + return new BigInteger(ByteBufferUtils.toByteArray(encoded)); + } + + private static int integerToInt(ByteBuffer encoded) throws Asn1DecodingException { + BigInteger value = integerToBigInteger(encoded); + if (value.compareTo(BigInteger.valueOf(Integer.MIN_VALUE)) < 0 + || value.compareTo(BigInteger.valueOf(Integer.MAX_VALUE)) > 0) { + throw new Asn1DecodingException( + String.format("INTEGER cannot be represented as int: %1$d (0x%1$x)", value)); + } + return value.intValue(); + } + + private static long integerToLong(ByteBuffer encoded) throws Asn1DecodingException { + BigInteger value = integerToBigInteger(encoded); + if (value.compareTo(BigInteger.valueOf(Long.MIN_VALUE)) < 0 + || value.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) > 0) { + throw new Asn1DecodingException( + String.format("INTEGER cannot be represented as long: %1$d (0x%1$x)", value)); + } + return value.longValue(); + } + + private static List<AnnotatedField> getAnnotatedFields(Class<?> containerClass) + throws Asn1DecodingException { + Field[] declaredFields = containerClass.getDeclaredFields(); + List<AnnotatedField> result = new ArrayList<>(declaredFields.length); + for (Field field : declaredFields) { + Asn1Field annotation = field.getDeclaredAnnotation(Asn1Field.class); + if (annotation == null) { + continue; + } + if (Modifier.isStatic(field.getModifiers())) { + throw new Asn1DecodingException( + Asn1Field.class.getName() + " used on a static field: " + + containerClass.getName() + "." + field.getName()); + } + + AnnotatedField annotatedField; + try { + annotatedField = new AnnotatedField(field, annotation); + } catch (Asn1DecodingException e) { + throw new Asn1DecodingException( + "Invalid ASN.1 annotation on " + + containerClass.getName() + "." + field.getName(), + e); + } + result.add(annotatedField); + } + return result; + } + + private static final class BerToJavaConverter { + private BerToJavaConverter() {} + + public static void setFieldValue( + Object obj, Field field, Asn1Type type, BerDataValue dataValue) + throws Asn1DecodingException { + try { + switch (type) { + case SET_OF: + case SEQUENCE_OF: + if (Asn1OpaqueObject.class.equals(field.getType())) { + field.set(obj, convert(type, dataValue, field.getType())); + } else { + field.set(obj, parseSetOf(dataValue, getElementType(field))); + } + return; + default: + field.set(obj, convert(type, dataValue, field.getType())); + break; + } + } catch (ReflectiveOperationException e) { + throw new Asn1DecodingException( + "Failed to set value of " + obj.getClass().getName() + + "." + field.getName(), + e); + } + } + + private static final byte[] EMPTY_BYTE_ARRAY = new byte[0]; + + @SuppressWarnings("unchecked") + public static <T> T convert( + Asn1Type sourceType, + BerDataValue dataValue, + Class<T> targetType) throws Asn1DecodingException { + if (ByteBuffer.class.equals(targetType)) { + return (T) dataValue.getEncodedContents(); + } else if (byte[].class.equals(targetType)) { + ByteBuffer resultBuf = dataValue.getEncodedContents(); + if (!resultBuf.hasRemaining()) { + return (T) EMPTY_BYTE_ARRAY; + } + byte[] result = new byte[resultBuf.remaining()]; + resultBuf.get(result); + return (T) result; + } else if (Asn1OpaqueObject.class.equals(targetType)) { + return (T) new Asn1OpaqueObject(dataValue.getEncoded()); + } + ByteBuffer encodedContents = dataValue.getEncodedContents(); + switch (sourceType) { + case INTEGER: + if ((int.class.equals(targetType)) || (Integer.class.equals(targetType))) { + return (T) Integer.valueOf(integerToInt(encodedContents)); + } else if ((long.class.equals(targetType)) || (Long.class.equals(targetType))) { + return (T) Long.valueOf(integerToLong(encodedContents)); + } else if (BigInteger.class.equals(targetType)) { + return (T) integerToBigInteger(encodedContents); + } + break; + case OBJECT_IDENTIFIER: + if (String.class.equals(targetType)) { + return (T) oidToString(encodedContents); + } + break; + case UTC_TIME: + case GENERALIZED_TIME: + if (String.class.equals(targetType)) { + return (T) new String(ByteBufferUtils.toByteArray(encodedContents)); + } + break; + case BOOLEAN: + // A boolean should be encoded in a single byte with a value of 0 for false and + // any non-zero value for true. + if (boolean.class.equals(targetType)) { + if (encodedContents.remaining() != 1) { + throw new Asn1DecodingException( + "Incorrect encoded size of boolean value: " + + encodedContents.remaining()); + } + boolean result; + if (encodedContents.get() == 0) { + result = false; + } else { + result = true; + } + return (T) new Boolean(result); + } + break; + case SEQUENCE: + { + Asn1Class containerAnnotation = + targetType.getDeclaredAnnotation(Asn1Class.class); + if ((containerAnnotation != null) + && (containerAnnotation.type() == Asn1Type.SEQUENCE)) { + return parseSequence(dataValue, targetType); + } + break; + } + case CHOICE: + { + Asn1Class containerAnnotation = + targetType.getDeclaredAnnotation(Asn1Class.class); + if ((containerAnnotation != null) + && (containerAnnotation.type() == Asn1Type.CHOICE)) { + return parseChoice(dataValue, targetType); + } + break; + } + default: + break; + } + + throw new Asn1DecodingException( + "Unsupported conversion: ASN.1 " + sourceType + " to " + targetType.getName()); + } + } +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1Class.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1Class.java new file mode 100644 index 0000000000..4841296c6b --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1Class.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.asn1; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface Asn1Class { + public Asn1Type type(); +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1DecodingException.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1DecodingException.java new file mode 100644 index 0000000000..07886429bf --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1DecodingException.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.asn1; + +/** + * Indicates that input could not be decoded into intended ASN.1 structure. + */ +public class Asn1DecodingException extends Exception { + private static final long serialVersionUID = 1L; + + public Asn1DecodingException(String message) { + super(message); + } + + public Asn1DecodingException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1DerEncoder.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1DerEncoder.java new file mode 100644 index 0000000000..901f5f30c0 --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1DerEncoder.java @@ -0,0 +1,596 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.asn1; + +import com.android.apksig.internal.asn1.ber.BerEncoding; + +import java.io.ByteArrayOutputStream; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +/** + * Encoder of ASN.1 structures into DER-encoded form. + * + * <p>Structure is described to the encoder by providing a class annotated with {@link Asn1Class}, + * containing fields annotated with {@link Asn1Field}. + */ +public final class Asn1DerEncoder { + private Asn1DerEncoder() {} + + /** + * Returns the DER-encoded form of the provided ASN.1 structure. + * + * @param container container to be encoded. The container's class must meet the following + * requirements: + * <ul> + * <li>The class must be annotated with {@link Asn1Class}.</li> + * <li>Member fields of the class which are to be encoded must be annotated with + * {@link Asn1Field} and be public.</li> + * </ul> + * + * @throws Asn1EncodingException if the input could not be encoded + */ + public static byte[] encode(Object container) throws Asn1EncodingException { + Class<?> containerClass = container.getClass(); + Asn1Class containerAnnotation = containerClass.getDeclaredAnnotation(Asn1Class.class); + if (containerAnnotation == null) { + throw new Asn1EncodingException( + containerClass.getName() + " not annotated with " + Asn1Class.class.getName()); + } + + Asn1Type containerType = containerAnnotation.type(); + switch (containerType) { + case CHOICE: + return toChoice(container); + case SEQUENCE: + return toSequence(container); + case UNENCODED_CONTAINER: + return toSequence(container, true); + default: + throw new Asn1EncodingException("Unsupported container type: " + containerType); + } + } + + private static byte[] toChoice(Object container) throws Asn1EncodingException { + Class<?> containerClass = container.getClass(); + List<AnnotatedField> fields = getAnnotatedFields(container); + if (fields.isEmpty()) { + throw new Asn1EncodingException( + "No fields annotated with " + Asn1Field.class.getName() + + " in CHOICE class " + containerClass.getName()); + } + + AnnotatedField resultField = null; + for (AnnotatedField field : fields) { + Object fieldValue = getMemberFieldValue(container, field.getField()); + if (fieldValue != null) { + if (resultField != null) { + throw new Asn1EncodingException( + "Multiple non-null fields in CHOICE class " + containerClass.getName() + + ": " + resultField.getField().getName() + + ", " + field.getField().getName()); + } + resultField = field; + } + } + + if (resultField == null) { + throw new Asn1EncodingException( + "No non-null fields in CHOICE class " + containerClass.getName()); + } + + return resultField.toDer(); + } + + private static byte[] toSequence(Object container) throws Asn1EncodingException { + return toSequence(container, false); + } + + private static byte[] toSequence(Object container, boolean omitTag) + throws Asn1EncodingException { + Class<?> containerClass = container.getClass(); + List<AnnotatedField> fields = getAnnotatedFields(container); + Collections.sort( + fields, (f1, f2) -> f1.getAnnotation().index() - f2.getAnnotation().index()); + if (fields.size() > 1) { + AnnotatedField lastField = null; + for (AnnotatedField field : fields) { + if ((lastField != null) + && (lastField.getAnnotation().index() == field.getAnnotation().index())) { + throw new Asn1EncodingException( + "Fields have the same index: " + containerClass.getName() + + "." + lastField.getField().getName() + + " and ." + field.getField().getName()); + } + lastField = field; + } + } + + List<byte[]> serializedFields = new ArrayList<>(fields.size()); + int contentLen = 0; + for (AnnotatedField field : fields) { + byte[] serializedField; + try { + serializedField = field.toDer(); + } catch (Asn1EncodingException e) { + throw new Asn1EncodingException( + "Failed to encode " + containerClass.getName() + + "." + field.getField().getName(), + e); + } + if (serializedField != null) { + serializedFields.add(serializedField); + contentLen += serializedField.length; + } + } + + if (omitTag) { + byte[] unencodedResult = new byte[contentLen]; + int index = 0; + for (byte[] serializedField : serializedFields) { + System.arraycopy(serializedField, 0, unencodedResult, index, serializedField.length); + index += serializedField.length; + } + return unencodedResult; + } else { + return createTag( + BerEncoding.TAG_CLASS_UNIVERSAL, true, BerEncoding.TAG_NUMBER_SEQUENCE, + serializedFields.toArray(new byte[0][])); + } + } + + private static byte[] toSetOf(Collection<?> values, Asn1Type elementType) throws Asn1EncodingException { + return toSequenceOrSetOf(values, elementType, true); + } + + private static byte[] toSequenceOf(Collection<?> values, Asn1Type elementType) throws Asn1EncodingException { + return toSequenceOrSetOf(values, elementType, false); + } + + private static byte[] toSequenceOrSetOf(Collection<?> values, Asn1Type elementType, boolean toSet) + throws Asn1EncodingException { + List<byte[]> serializedValues = new ArrayList<>(values.size()); + for (Object value : values) { + serializedValues.add(JavaToDerConverter.toDer(value, elementType, null)); + } + int tagNumber; + if (toSet) { + if (serializedValues.size() > 1) { + Collections.sort(serializedValues, ByteArrayLexicographicComparator.INSTANCE); + } + tagNumber = BerEncoding.TAG_NUMBER_SET; + } else { + tagNumber = BerEncoding.TAG_NUMBER_SEQUENCE; + } + return createTag( + BerEncoding.TAG_CLASS_UNIVERSAL, true, tagNumber, + serializedValues.toArray(new byte[0][])); + } + + /** + * Compares two bytes arrays based on their lexicographic order. Corresponding elements of the + * two arrays are compared in ascending order. Elements at out of range indices are assumed to + * be smaller than the smallest possible value for an element. + */ + private static class ByteArrayLexicographicComparator implements Comparator<byte[]> { + private static final ByteArrayLexicographicComparator INSTANCE = + new ByteArrayLexicographicComparator(); + + @Override + public int compare(byte[] arr1, byte[] arr2) { + int commonLength = Math.min(arr1.length, arr2.length); + for (int i = 0; i < commonLength; i++) { + int diff = (arr1[i] & 0xff) - (arr2[i] & 0xff); + if (diff != 0) { + return diff; + } + } + return arr1.length - arr2.length; + } + } + + private static List<AnnotatedField> getAnnotatedFields(Object container) + throws Asn1EncodingException { + Class<?> containerClass = container.getClass(); + Field[] declaredFields = containerClass.getDeclaredFields(); + List<AnnotatedField> result = new ArrayList<>(declaredFields.length); + for (Field field : declaredFields) { + Asn1Field annotation = field.getDeclaredAnnotation(Asn1Field.class); + if (annotation == null) { + continue; + } + if (Modifier.isStatic(field.getModifiers())) { + throw new Asn1EncodingException( + Asn1Field.class.getName() + " used on a static field: " + + containerClass.getName() + "." + field.getName()); + } + + AnnotatedField annotatedField; + try { + annotatedField = new AnnotatedField(container, field, annotation); + } catch (Asn1EncodingException e) { + throw new Asn1EncodingException( + "Invalid ASN.1 annotation on " + + containerClass.getName() + "." + field.getName(), + e); + } + result.add(annotatedField); + } + return result; + } + + private static byte[] toInteger(int value) { + return toInteger((long) value); + } + + private static byte[] toInteger(long value) { + return toInteger(BigInteger.valueOf(value)); + } + + private static byte[] toInteger(BigInteger value) { + return createTag( + BerEncoding.TAG_CLASS_UNIVERSAL, false, BerEncoding.TAG_NUMBER_INTEGER, + value.toByteArray()); + } + + private static byte[] toBoolean(boolean value) { + // A boolean should be encoded in a single byte with a value of 0 for false and any non-zero + // value for true. + byte[] result = new byte[1]; + if (value == false) { + result[0] = 0; + } else { + result[0] = 1; + } + return createTag(BerEncoding.TAG_CLASS_UNIVERSAL, false, BerEncoding.TAG_NUMBER_BOOLEAN, result); + } + + private static byte[] toOid(String oid) throws Asn1EncodingException { + ByteArrayOutputStream encodedValue = new ByteArrayOutputStream(); + String[] nodes = oid.split("\\."); + if (nodes.length < 2) { + throw new Asn1EncodingException( + "OBJECT IDENTIFIER must contain at least two nodes: " + oid); + } + int firstNode; + try { + firstNode = Integer.parseInt(nodes[0]); + } catch (NumberFormatException e) { + throw new Asn1EncodingException("Node #1 not numeric: " + nodes[0]); + } + if ((firstNode > 6) || (firstNode < 0)) { + throw new Asn1EncodingException("Invalid value for node #1: " + firstNode); + } + + int secondNode; + try { + secondNode = Integer.parseInt(nodes[1]); + } catch (NumberFormatException e) { + throw new Asn1EncodingException("Node #2 not numeric: " + nodes[1]); + } + if ((secondNode >= 40) || (secondNode < 0)) { + throw new Asn1EncodingException("Invalid value for node #2: " + secondNode); + } + int firstByte = firstNode * 40 + secondNode; + if (firstByte > 0xff) { + throw new Asn1EncodingException( + "First two nodes out of range: " + firstNode + "." + secondNode); + } + + encodedValue.write(firstByte); + for (int i = 2; i < nodes.length; i++) { + String nodeString = nodes[i]; + int node; + try { + node = Integer.parseInt(nodeString); + } catch (NumberFormatException e) { + throw new Asn1EncodingException("Node #" + (i + 1) + " not numeric: " + nodeString); + } + if (node < 0) { + throw new Asn1EncodingException("Invalid value for node #" + (i + 1) + ": " + node); + } + if (node <= 0x7f) { + encodedValue.write(node); + continue; + } + if (node < 1 << 14) { + encodedValue.write(0x80 | (node >> 7)); + encodedValue.write(node & 0x7f); + continue; + } + if (node < 1 << 21) { + encodedValue.write(0x80 | (node >> 14)); + encodedValue.write(0x80 | ((node >> 7) & 0x7f)); + encodedValue.write(node & 0x7f); + continue; + } + throw new Asn1EncodingException("Node #" + (i + 1) + " too large: " + node); + } + + return createTag( + BerEncoding.TAG_CLASS_UNIVERSAL, false, BerEncoding.TAG_NUMBER_OBJECT_IDENTIFIER, + encodedValue.toByteArray()); + } + + private static Object getMemberFieldValue(Object obj, Field field) + throws Asn1EncodingException { + try { + return field.get(obj); + } catch (ReflectiveOperationException e) { + throw new Asn1EncodingException( + "Failed to read " + obj.getClass().getName() + "." + field.getName(), e); + } + } + + private static final class AnnotatedField { + private final Field mField; + private final Object mObject; + private final Asn1Field mAnnotation; + private final Asn1Type mDataType; + private final Asn1Type mElementDataType; + private final Asn1TagClass mTagClass; + private final int mDerTagClass; + private final int mDerTagNumber; + private final Asn1Tagging mTagging; + private final boolean mOptional; + + public AnnotatedField(Object obj, Field field, Asn1Field annotation) + throws Asn1EncodingException { + mObject = obj; + mField = field; + mAnnotation = annotation; + mDataType = annotation.type(); + mElementDataType = annotation.elementType(); + + Asn1TagClass tagClass = annotation.cls(); + if (tagClass == Asn1TagClass.AUTOMATIC) { + if (annotation.tagNumber() != -1) { + tagClass = Asn1TagClass.CONTEXT_SPECIFIC; + } else { + tagClass = Asn1TagClass.UNIVERSAL; + } + } + mTagClass = tagClass; + mDerTagClass = BerEncoding.getTagClass(mTagClass); + + int tagNumber; + if (annotation.tagNumber() != -1) { + tagNumber = annotation.tagNumber(); + } else if ((mDataType == Asn1Type.CHOICE) || (mDataType == Asn1Type.ANY)) { + tagNumber = -1; + } else { + tagNumber = BerEncoding.getTagNumber(mDataType); + } + mDerTagNumber = tagNumber; + + mTagging = annotation.tagging(); + if (((mTagging == Asn1Tagging.EXPLICIT) || (mTagging == Asn1Tagging.IMPLICIT)) + && (annotation.tagNumber() == -1)) { + throw new Asn1EncodingException( + "Tag number must be specified when tagging mode is " + mTagging); + } + + mOptional = annotation.optional(); + } + + public Field getField() { + return mField; + } + + public Asn1Field getAnnotation() { + return mAnnotation; + } + + public byte[] toDer() throws Asn1EncodingException { + Object fieldValue = getMemberFieldValue(mObject, mField); + if (fieldValue == null) { + if (mOptional) { + return null; + } + throw new Asn1EncodingException("Required field not set"); + } + + byte[] encoded = JavaToDerConverter.toDer(fieldValue, mDataType, mElementDataType); + switch (mTagging) { + case NORMAL: + return encoded; + case EXPLICIT: + return createTag(mDerTagClass, true, mDerTagNumber, encoded); + case IMPLICIT: + int originalTagNumber = BerEncoding.getTagNumber(encoded[0]); + if (originalTagNumber == 0x1f) { + throw new Asn1EncodingException("High-tag-number form not supported"); + } + if (mDerTagNumber >= 0x1f) { + throw new Asn1EncodingException( + "Unsupported high tag number: " + mDerTagNumber); + } + encoded[0] = BerEncoding.setTagNumber(encoded[0], mDerTagNumber); + encoded[0] = BerEncoding.setTagClass(encoded[0], mDerTagClass); + return encoded; + default: + throw new RuntimeException("Unknown tagging mode: " + mTagging); + } + } + } + + private static byte[] createTag( + int tagClass, boolean constructed, int tagNumber, byte[]... contents) { + if (tagNumber >= 0x1f) { + throw new IllegalArgumentException("High tag numbers not supported: " + tagNumber); + } + // tag class & number fit into the first byte + byte firstIdentifierByte = + (byte) ((tagClass << 6) | (constructed ? 1 << 5 : 0) | tagNumber); + + int contentsLength = 0; + for (byte[] c : contents) { + contentsLength += c.length; + } + int contentsPosInResult; + byte[] result; + if (contentsLength < 0x80) { + // Length fits into one byte + contentsPosInResult = 2; + result = new byte[contentsPosInResult + contentsLength]; + result[0] = firstIdentifierByte; + result[1] = (byte) contentsLength; + } else { + // Length is represented as multiple bytes + // The low 7 bits of the first byte represent the number of length bytes (following the + // first byte) in which the length is in big-endian base-256 form + if (contentsLength <= 0xff) { + contentsPosInResult = 3; + result = new byte[contentsPosInResult + contentsLength]; + result[1] = (byte) 0x81; // 1 length byte + result[2] = (byte) contentsLength; + } else if (contentsLength <= 0xffff) { + contentsPosInResult = 4; + result = new byte[contentsPosInResult + contentsLength]; + result[1] = (byte) 0x82; // 2 length bytes + result[2] = (byte) (contentsLength >> 8); + result[3] = (byte) (contentsLength & 0xff); + } else if (contentsLength <= 0xffffff) { + contentsPosInResult = 5; + result = new byte[contentsPosInResult + contentsLength]; + result[1] = (byte) 0x83; // 3 length bytes + result[2] = (byte) (contentsLength >> 16); + result[3] = (byte) ((contentsLength >> 8) & 0xff); + result[4] = (byte) (contentsLength & 0xff); + } else { + contentsPosInResult = 6; + result = new byte[contentsPosInResult + contentsLength]; + result[1] = (byte) 0x84; // 4 length bytes + result[2] = (byte) (contentsLength >> 24); + result[3] = (byte) ((contentsLength >> 16) & 0xff); + result[4] = (byte) ((contentsLength >> 8) & 0xff); + result[5] = (byte) (contentsLength & 0xff); + } + result[0] = firstIdentifierByte; + } + for (byte[] c : contents) { + System.arraycopy(c, 0, result, contentsPosInResult, c.length); + contentsPosInResult += c.length; + } + return result; + } + + private static final class JavaToDerConverter { + private JavaToDerConverter() {} + + public static byte[] toDer(Object source, Asn1Type targetType, Asn1Type targetElementType) + throws Asn1EncodingException { + Class<?> sourceType = source.getClass(); + if (Asn1OpaqueObject.class.equals(sourceType)) { + ByteBuffer buf = ((Asn1OpaqueObject) source).getEncoded(); + byte[] result = new byte[buf.remaining()]; + buf.get(result); + return result; + } + + if ((targetType == null) || (targetType == Asn1Type.ANY)) { + return encode(source); + } + + switch (targetType) { + case OCTET_STRING: + case BIT_STRING: + byte[] value = null; + if (source instanceof ByteBuffer) { + ByteBuffer buf = (ByteBuffer) source; + value = new byte[buf.remaining()]; + buf.slice().get(value); + } else if (source instanceof byte[]) { + value = (byte[]) source; + } + if (value != null) { + return createTag( + BerEncoding.TAG_CLASS_UNIVERSAL, + false, + BerEncoding.getTagNumber(targetType), + value); + } + break; + case INTEGER: + if (source instanceof Integer) { + return toInteger((Integer) source); + } else if (source instanceof Long) { + return toInteger((Long) source); + } else if (source instanceof BigInteger) { + return toInteger((BigInteger) source); + } + break; + case BOOLEAN: + if (source instanceof Boolean) { + return toBoolean((Boolean) (source)); + } + break; + case UTC_TIME: + case GENERALIZED_TIME: + if (source instanceof String) { + return createTag(BerEncoding.TAG_CLASS_UNIVERSAL, false, + BerEncoding.getTagNumber(targetType), ((String) source).getBytes()); + } + break; + case OBJECT_IDENTIFIER: + if (source instanceof String) { + return toOid((String) source); + } + break; + case SEQUENCE: + { + Asn1Class containerAnnotation = + sourceType.getDeclaredAnnotation(Asn1Class.class); + if ((containerAnnotation != null) + && (containerAnnotation.type() == Asn1Type.SEQUENCE)) { + return toSequence(source); + } + break; + } + case CHOICE: + { + Asn1Class containerAnnotation = + sourceType.getDeclaredAnnotation(Asn1Class.class); + if ((containerAnnotation != null) + && (containerAnnotation.type() == Asn1Type.CHOICE)) { + return toChoice(source); + } + break; + } + case SET_OF: + return toSetOf((Collection<?>) source, targetElementType); + case SEQUENCE_OF: + return toSequenceOf((Collection<?>) source, targetElementType); + default: + break; + } + + throw new Asn1EncodingException( + "Unsupported conversion: " + sourceType.getName() + " to ASN.1 " + targetType); + } + } + /** ASN.1 DER-encoded {@code NULL}. */ + public static final Asn1OpaqueObject ASN1_DER_NULL = + new Asn1OpaqueObject(new byte[] {BerEncoding.TAG_NUMBER_NULL, 0}); +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1EncodingException.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1EncodingException.java new file mode 100644 index 0000000000..0002c25cba --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1EncodingException.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.asn1; + +/** + * Indicates that an ASN.1 structure could not be encoded. + */ +public class Asn1EncodingException extends Exception { + private static final long serialVersionUID = 1L; + + public Asn1EncodingException(String message) { + super(message); + } + + public Asn1EncodingException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1Field.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1Field.java new file mode 100644 index 0000000000..d2d3ce049e --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1Field.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.asn1; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface Asn1Field { + /** Index used to order fields in a container. Required for fields of SEQUENCE containers. */ + public int index() default 0; + + public Asn1TagClass cls() default Asn1TagClass.AUTOMATIC; + + public Asn1Type type(); + + /** Tagging mode. Default: NORMAL. */ + public Asn1Tagging tagging() default Asn1Tagging.NORMAL; + + /** Tag number. Required when IMPLICIT and EXPLICIT tagging mode is used.*/ + public int tagNumber() default -1; + + /** {@code true} if this field is optional. Ignored for fields of CHOICE containers. */ + public boolean optional() default false; + + /** Type of elements. Used only for SET_OF or SEQUENCE_OF. */ + public Asn1Type elementType() default Asn1Type.ANY; +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1OpaqueObject.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1OpaqueObject.java new file mode 100644 index 0000000000..672d0e74c6 --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1OpaqueObject.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.asn1; + +import java.nio.ByteBuffer; + +/** + * Opaque holder of encoded ASN.1 stuff. + */ +public class Asn1OpaqueObject { + private final ByteBuffer mEncoded; + + public Asn1OpaqueObject(ByteBuffer encoded) { + mEncoded = encoded.slice(); + } + + public Asn1OpaqueObject(byte[] encoded) { + mEncoded = ByteBuffer.wrap(encoded); + } + + public ByteBuffer getEncoded() { + return mEncoded.slice(); + } +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1TagClass.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1TagClass.java new file mode 100644 index 0000000000..6cdfcf014c --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1TagClass.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.asn1; + +public enum Asn1TagClass { + UNIVERSAL, + APPLICATION, + CONTEXT_SPECIFIC, + PRIVATE, + + /** + * Not really an actual tag class: decoder/encoder will attempt to deduce the correct tag class + * automatically. + */ + AUTOMATIC, +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1Tagging.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1Tagging.java new file mode 100644 index 0000000000..35fa3744e1 --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1Tagging.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.asn1; + +public enum Asn1Tagging { + NORMAL, + EXPLICIT, + IMPLICIT, +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1Type.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1Type.java new file mode 100644 index 0000000000..73006222b2 --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1Type.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.asn1; + +public enum Asn1Type { + ANY, + CHOICE, + INTEGER, + OBJECT_IDENTIFIER, + OCTET_STRING, + SEQUENCE, + SEQUENCE_OF, + SET_OF, + BIT_STRING, + UTC_TIME, + GENERALIZED_TIME, + BOOLEAN, + // This type can be used to annotate classes that encapsulate ASN.1 structures that are not + // classified as a SEQUENCE or SET. + UNENCODED_CONTAINER +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/ber/BerDataValue.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/ber/BerDataValue.java new file mode 100644 index 0000000000..f5604ffdfb --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/ber/BerDataValue.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.asn1.ber; + +import java.nio.ByteBuffer; + +/** + * ASN.1 Basic Encoding Rules (BER) data value -- see {@code X.690}. + */ +public class BerDataValue { + private final ByteBuffer mEncoded; + private final ByteBuffer mEncodedContents; + private final int mTagClass; + private final boolean mConstructed; + private final int mTagNumber; + + BerDataValue( + ByteBuffer encoded, + ByteBuffer encodedContents, + int tagClass, + boolean constructed, + int tagNumber) { + mEncoded = encoded; + mEncodedContents = encodedContents; + mTagClass = tagClass; + mConstructed = constructed; + mTagNumber = tagNumber; + } + + /** + * Returns the tag class of this data value. See {@link BerEncoding} {@code TAG_CLASS} + * constants. + */ + public int getTagClass() { + return mTagClass; + } + + /** + * Returns {@code true} if the content octets of this data value are the complete BER encoding + * of one or more data values, {@code false} if the content octets of this data value directly + * represent the value. + */ + public boolean isConstructed() { + return mConstructed; + } + + /** + * Returns the tag number of this data value. See {@link BerEncoding} {@code TAG_NUMBER} + * constants. + */ + public int getTagNumber() { + return mTagNumber; + } + + /** + * Returns the encoded form of this data value. + */ + public ByteBuffer getEncoded() { + return mEncoded.slice(); + } + + /** + * Returns the encoded contents of this data value. + */ + public ByteBuffer getEncodedContents() { + return mEncodedContents.slice(); + } + + /** + * Returns a new reader of the contents of this data value. + */ + public BerDataValueReader contentsReader() { + return new ByteBufferBerDataValueReader(getEncodedContents()); + } + + /** + * Returns a new reader which returns just this data value. This may be useful for re-reading + * this value in different contexts. + */ + public BerDataValueReader dataValueReader() { + return new ParsedValueReader(this); + } + + private static final class ParsedValueReader implements BerDataValueReader { + private final BerDataValue mValue; + private boolean mValueOutput; + + public ParsedValueReader(BerDataValue value) { + mValue = value; + } + + @Override + public BerDataValue readDataValue() throws BerDataValueFormatException { + if (mValueOutput) { + return null; + } + mValueOutput = true; + return mValue; + } + } +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/ber/BerDataValueFormatException.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/ber/BerDataValueFormatException.java new file mode 100644 index 0000000000..11ef6c3672 --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/ber/BerDataValueFormatException.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.asn1.ber; + +/** + * Indicates that an ASN.1 data value being read could not be decoded using + * Basic Encoding Rules (BER). + */ +public class BerDataValueFormatException extends Exception { + + private static final long serialVersionUID = 1L; + + public BerDataValueFormatException(String message) { + super(message); + } + + public BerDataValueFormatException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/ber/BerDataValueReader.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/ber/BerDataValueReader.java new file mode 100644 index 0000000000..8da0a428be --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/ber/BerDataValueReader.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.asn1.ber; + +/** + * Reader of ASN.1 Basic Encoding Rules (BER) data values. + * + * <p>BER data value reader returns data values, one by one, from a source. The interpretation of + * data values (e.g., how to obtain a numeric value from an INTEGER data value, or how to extract + * the elements of a SEQUENCE value) is left to clients of the reader. + */ +public interface BerDataValueReader { + + /** + * Returns the next data value or {@code null} if end of input has been reached. + * + * @throws BerDataValueFormatException if the value being read is malformed. + */ + BerDataValue readDataValue() throws BerDataValueFormatException; +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/ber/BerEncoding.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/ber/BerEncoding.java new file mode 100644 index 0000000000..d32330c0ad --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/ber/BerEncoding.java @@ -0,0 +1,225 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.asn1.ber; + +import com.android.apksig.internal.asn1.Asn1Type; +import com.android.apksig.internal.asn1.Asn1TagClass; + +/** + * ASN.1 Basic Encoding Rules (BER) constants and helper methods. See {@code X.690}. + */ +public abstract class BerEncoding { + private BerEncoding() {} + + /** + * Constructed vs primitive flag in the first identifier byte. + */ + public static final int ID_FLAG_CONSTRUCTED_ENCODING = 1 << 5; + + /** + * Tag class: UNIVERSAL + */ + public static final int TAG_CLASS_UNIVERSAL = 0; + + /** + * Tag class: APPLICATION + */ + public static final int TAG_CLASS_APPLICATION = 1; + + /** + * Tag class: CONTEXT SPECIFIC + */ + public static final int TAG_CLASS_CONTEXT_SPECIFIC = 2; + + /** + * Tag class: PRIVATE + */ + public static final int TAG_CLASS_PRIVATE = 3; + + /** + * Tag number: BOOLEAN + */ + public static final int TAG_NUMBER_BOOLEAN = 0x1; + + /** + * Tag number: INTEGER + */ + public static final int TAG_NUMBER_INTEGER = 0x2; + + /** + * Tag number: BIT STRING + */ + public static final int TAG_NUMBER_BIT_STRING = 0x3; + + /** + * Tag number: OCTET STRING + */ + public static final int TAG_NUMBER_OCTET_STRING = 0x4; + + /** + * Tag number: NULL + */ + public static final int TAG_NUMBER_NULL = 0x05; + + /** + * Tag number: OBJECT IDENTIFIER + */ + public static final int TAG_NUMBER_OBJECT_IDENTIFIER = 0x6; + + /** + * Tag number: SEQUENCE + */ + public static final int TAG_NUMBER_SEQUENCE = 0x10; + + /** + * Tag number: SET + */ + public static final int TAG_NUMBER_SET = 0x11; + + /** + * Tag number: UTC_TIME + */ + public final static int TAG_NUMBER_UTC_TIME = 0x17; + + /** + * Tag number: GENERALIZED_TIME + */ + public final static int TAG_NUMBER_GENERALIZED_TIME = 0x18; + + public static int getTagNumber(Asn1Type dataType) { + switch (dataType) { + case INTEGER: + return TAG_NUMBER_INTEGER; + case OBJECT_IDENTIFIER: + return TAG_NUMBER_OBJECT_IDENTIFIER; + case OCTET_STRING: + return TAG_NUMBER_OCTET_STRING; + case BIT_STRING: + return TAG_NUMBER_BIT_STRING; + case SET_OF: + return TAG_NUMBER_SET; + case SEQUENCE: + case SEQUENCE_OF: + return TAG_NUMBER_SEQUENCE; + case UTC_TIME: + return TAG_NUMBER_UTC_TIME; + case GENERALIZED_TIME: + return TAG_NUMBER_GENERALIZED_TIME; + case BOOLEAN: + return TAG_NUMBER_BOOLEAN; + default: + throw new IllegalArgumentException("Unsupported data type: " + dataType); + } + } + + public static int getTagClass(Asn1TagClass tagClass) { + switch (tagClass) { + case APPLICATION: + return TAG_CLASS_APPLICATION; + case CONTEXT_SPECIFIC: + return TAG_CLASS_CONTEXT_SPECIFIC; + case PRIVATE: + return TAG_CLASS_PRIVATE; + case UNIVERSAL: + return TAG_CLASS_UNIVERSAL; + default: + throw new IllegalArgumentException("Unsupported tag class: " + tagClass); + } + } + + public static String tagClassToString(int typeClass) { + switch (typeClass) { + case TAG_CLASS_APPLICATION: + return "APPLICATION"; + case TAG_CLASS_CONTEXT_SPECIFIC: + return ""; + case TAG_CLASS_PRIVATE: + return "PRIVATE"; + case TAG_CLASS_UNIVERSAL: + return "UNIVERSAL"; + default: + throw new IllegalArgumentException("Unsupported type class: " + typeClass); + } + } + + public static String tagClassAndNumberToString(int tagClass, int tagNumber) { + String classString = tagClassToString(tagClass); + String numberString = tagNumberToString(tagNumber); + return classString.isEmpty() ? numberString : classString + " " + numberString; + } + + + public static String tagNumberToString(int tagNumber) { + switch (tagNumber) { + case TAG_NUMBER_INTEGER: + return "INTEGER"; + case TAG_NUMBER_OCTET_STRING: + return "OCTET STRING"; + case TAG_NUMBER_BIT_STRING: + return "BIT STRING"; + case TAG_NUMBER_NULL: + return "NULL"; + case TAG_NUMBER_OBJECT_IDENTIFIER: + return "OBJECT IDENTIFIER"; + case TAG_NUMBER_SEQUENCE: + return "SEQUENCE"; + case TAG_NUMBER_SET: + return "SET"; + case TAG_NUMBER_BOOLEAN: + return "BOOLEAN"; + case TAG_NUMBER_GENERALIZED_TIME: + return "GENERALIZED TIME"; + case TAG_NUMBER_UTC_TIME: + return "UTC TIME"; + default: + return "0x" + Integer.toHexString(tagNumber); + } + } + + /** + * Returns {@code true} if the provided first identifier byte indicates that the data value uses + * constructed encoding for its contents, or {@code false} if the data value uses primitive + * encoding for its contents. + */ + public static boolean isConstructed(byte firstIdentifierByte) { + return (firstIdentifierByte & ID_FLAG_CONSTRUCTED_ENCODING) != 0; + } + + /** + * Returns the tag class encoded in the provided first identifier byte. See {@code TAG_CLASS} + * constants. + */ + public static int getTagClass(byte firstIdentifierByte) { + return (firstIdentifierByte & 0xff) >> 6; + } + + public static byte setTagClass(byte firstIdentifierByte, int tagClass) { + return (byte) ((firstIdentifierByte & 0x3f) | (tagClass << 6)); + } + + /** + * Returns the tag number encoded in the provided first identifier byte. See {@code TAG_NUMBER} + * constants. + */ + public static int getTagNumber(byte firstIdentifierByte) { + return firstIdentifierByte & 0x1f; + } + + public static byte setTagNumber(byte firstIdentifierByte, int tagNumber) { + return (byte) ((firstIdentifierByte & ~0x1f) | tagNumber); + } +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/ber/ByteBufferBerDataValueReader.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/ber/ByteBufferBerDataValueReader.java new file mode 100644 index 0000000000..3fd5291f06 --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/ber/ByteBufferBerDataValueReader.java @@ -0,0 +1,208 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.asn1.ber; + +import java.nio.ByteBuffer; + +/** + * {@link BerDataValueReader} which reads from a {@link ByteBuffer} containing BER-encoded data + * values. See {@code X.690} for the encoding. + */ +public class ByteBufferBerDataValueReader implements BerDataValueReader { + private final ByteBuffer mBuf; + + public ByteBufferBerDataValueReader(ByteBuffer buf) { + if (buf == null) { + throw new NullPointerException("buf == null"); + } + mBuf = buf; + } + + @Override + public BerDataValue readDataValue() throws BerDataValueFormatException { + int startPosition = mBuf.position(); + if (!mBuf.hasRemaining()) { + return null; + } + byte firstIdentifierByte = mBuf.get(); + int tagNumber = readTagNumber(firstIdentifierByte); + boolean constructed = BerEncoding.isConstructed(firstIdentifierByte); + + if (!mBuf.hasRemaining()) { + throw new BerDataValueFormatException("Missing length"); + } + int firstLengthByte = mBuf.get() & 0xff; + int contentsLength; + int contentsOffsetInTag; + if ((firstLengthByte & 0x80) == 0) { + // short form length + contentsLength = readShortFormLength(firstLengthByte); + contentsOffsetInTag = mBuf.position() - startPosition; + skipDefiniteLengthContents(contentsLength); + } else if (firstLengthByte != 0x80) { + // long form length + contentsLength = readLongFormLength(firstLengthByte); + contentsOffsetInTag = mBuf.position() - startPosition; + skipDefiniteLengthContents(contentsLength); + } else { + // indefinite length -- value ends with 0x00 0x00 + contentsOffsetInTag = mBuf.position() - startPosition; + contentsLength = + constructed + ? skipConstructedIndefiniteLengthContents() + : skipPrimitiveIndefiniteLengthContents(); + } + + // Create the encoded data value ByteBuffer + int endPosition = mBuf.position(); + mBuf.position(startPosition); + int bufOriginalLimit = mBuf.limit(); + mBuf.limit(endPosition); + ByteBuffer encoded = mBuf.slice(); + mBuf.position(mBuf.limit()); + mBuf.limit(bufOriginalLimit); + + // Create the encoded contents ByteBuffer + encoded.position(contentsOffsetInTag); + encoded.limit(contentsOffsetInTag + contentsLength); + ByteBuffer encodedContents = encoded.slice(); + encoded.clear(); + + return new BerDataValue( + encoded, + encodedContents, + BerEncoding.getTagClass(firstIdentifierByte), + constructed, + tagNumber); + } + + private int readTagNumber(byte firstIdentifierByte) throws BerDataValueFormatException { + int tagNumber = BerEncoding.getTagNumber(firstIdentifierByte); + if (tagNumber == 0x1f) { + // high-tag-number form, where the tag number follows this byte in base-128 + // big-endian form, where each byte has the highest bit set, except for the last + // byte + return readHighTagNumber(); + } else { + // low-tag-number form + return tagNumber; + } + } + + private int readHighTagNumber() throws BerDataValueFormatException { + // Base-128 big-endian form, where each byte has the highest bit set, except for the last + // byte + int b; + int result = 0; + do { + if (!mBuf.hasRemaining()) { + throw new BerDataValueFormatException("Truncated tag number"); + } + b = mBuf.get(); + if (result > Integer.MAX_VALUE >>> 7) { + throw new BerDataValueFormatException("Tag number too large"); + } + result <<= 7; + result |= b & 0x7f; + } while ((b & 0x80) != 0); + return result; + } + + private int readShortFormLength(int firstLengthByte) { + return firstLengthByte & 0x7f; + } + + private int readLongFormLength(int firstLengthByte) throws BerDataValueFormatException { + // The low 7 bits of the first byte represent the number of bytes (following the first + // byte) in which the length is in big-endian base-256 form + int byteCount = firstLengthByte & 0x7f; + if (byteCount > 4) { + throw new BerDataValueFormatException("Length too large: " + byteCount + " bytes"); + } + int result = 0; + for (int i = 0; i < byteCount; i++) { + if (!mBuf.hasRemaining()) { + throw new BerDataValueFormatException("Truncated length"); + } + int b = mBuf.get(); + if (result > Integer.MAX_VALUE >>> 8) { + throw new BerDataValueFormatException("Length too large"); + } + result <<= 8; + result |= b & 0xff; + } + return result; + } + + private void skipDefiniteLengthContents(int contentsLength) throws BerDataValueFormatException { + if (mBuf.remaining() < contentsLength) { + throw new BerDataValueFormatException( + "Truncated contents. Need: " + contentsLength + " bytes, available: " + + mBuf.remaining()); + } + mBuf.position(mBuf.position() + contentsLength); + } + + private int skipPrimitiveIndefiniteLengthContents() throws BerDataValueFormatException { + // Contents are terminated by 0x00 0x00 + boolean prevZeroByte = false; + int bytesRead = 0; + while (true) { + if (!mBuf.hasRemaining()) { + throw new BerDataValueFormatException( + "Truncated indefinite-length contents: " + bytesRead + " bytes read"); + + } + int b = mBuf.get(); + bytesRead++; + if (bytesRead < 0) { + throw new BerDataValueFormatException("Indefinite-length contents too long"); + } + if (b == 0) { + if (prevZeroByte) { + // End of contents reached -- we've read the value and its terminator 0x00 0x00 + return bytesRead - 2; + } + prevZeroByte = true; + } else { + prevZeroByte = false; + } + } + } + + private int skipConstructedIndefiniteLengthContents() throws BerDataValueFormatException { + // Contents are terminated by 0x00 0x00. However, this data value is constructed, meaning it + // can contain data values which are themselves indefinite length encoded. As a result, we + // must parse the direct children of this data value to correctly skip over the contents of + // this data value. + int startPos = mBuf.position(); + while (mBuf.hasRemaining()) { + // Check whether the 0x00 0x00 terminator is at current position + if ((mBuf.remaining() > 1) && (mBuf.getShort(mBuf.position()) == 0)) { + int contentsLength = mBuf.position() - startPos; + mBuf.position(mBuf.position() + 2); + return contentsLength; + } + // No luck. This must be a BER-encoded data value -- skip over it by parsing it + readDataValue(); + } + + throw new BerDataValueFormatException( + "Truncated indefinite-length contents: " + + (mBuf.position() - startPos) + " bytes read"); + } +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/ber/InputStreamBerDataValueReader.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/ber/InputStreamBerDataValueReader.java new file mode 100644 index 0000000000..5fbca51db3 --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/ber/InputStreamBerDataValueReader.java @@ -0,0 +1,313 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.asn1.ber; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; + +/** + * {@link BerDataValueReader} which reads from an {@link InputStream} returning BER-encoded data + * values. See {@code X.690} for the encoding. + */ +public class InputStreamBerDataValueReader implements BerDataValueReader { + private final InputStream mIn; + + public InputStreamBerDataValueReader(InputStream in) { + if (in == null) { + throw new NullPointerException("in == null"); + } + mIn = in; + } + + @Override + public BerDataValue readDataValue() throws BerDataValueFormatException { + return readDataValue(mIn); + } + + /** + * Returns the next data value or {@code null} if end of input has been reached. + * + * @throws BerDataValueFormatException if the value being read is malformed. + */ + @SuppressWarnings("resource") + private static BerDataValue readDataValue(InputStream input) + throws BerDataValueFormatException { + RecordingInputStream in = new RecordingInputStream(input); + + try { + int firstIdentifierByte = in.read(); + if (firstIdentifierByte == -1) { + // End of input + return null; + } + int tagNumber = readTagNumber(in, firstIdentifierByte); + + int firstLengthByte = in.read(); + if (firstLengthByte == -1) { + throw new BerDataValueFormatException("Missing length"); + } + + boolean constructed = BerEncoding.isConstructed((byte) firstIdentifierByte); + int contentsLength; + int contentsOffsetInDataValue; + if ((firstLengthByte & 0x80) == 0) { + // short form length + contentsLength = readShortFormLength(firstLengthByte); + contentsOffsetInDataValue = in.getReadByteCount(); + skipDefiniteLengthContents(in, contentsLength); + } else if ((firstLengthByte & 0xff) != 0x80) { + // long form length + contentsLength = readLongFormLength(in, firstLengthByte); + contentsOffsetInDataValue = in.getReadByteCount(); + skipDefiniteLengthContents(in, contentsLength); + } else { + // indefinite length + contentsOffsetInDataValue = in.getReadByteCount(); + contentsLength = + constructed + ? skipConstructedIndefiniteLengthContents(in) + : skipPrimitiveIndefiniteLengthContents(in); + } + + byte[] encoded = in.getReadBytes(); + ByteBuffer encodedContents = + ByteBuffer.wrap(encoded, contentsOffsetInDataValue, contentsLength); + return new BerDataValue( + ByteBuffer.wrap(encoded), + encodedContents, + BerEncoding.getTagClass((byte) firstIdentifierByte), + constructed, + tagNumber); + } catch (IOException e) { + throw new BerDataValueFormatException("Failed to read data value", e); + } + } + + private static int readTagNumber(InputStream in, int firstIdentifierByte) + throws IOException, BerDataValueFormatException { + int tagNumber = BerEncoding.getTagNumber((byte) firstIdentifierByte); + if (tagNumber == 0x1f) { + // high-tag-number form + return readHighTagNumber(in); + } else { + // low-tag-number form + return tagNumber; + } + } + + private static int readHighTagNumber(InputStream in) + throws IOException, BerDataValueFormatException { + // Base-128 big-endian form, where each byte has the highest bit set, except for the last + // byte where the highest bit is not set + int b; + int result = 0; + do { + b = in.read(); + if (b == -1) { + throw new BerDataValueFormatException("Truncated tag number"); + } + if (result > Integer.MAX_VALUE >>> 7) { + throw new BerDataValueFormatException("Tag number too large"); + } + result <<= 7; + result |= b & 0x7f; + } while ((b & 0x80) != 0); + return result; + } + + private static int readShortFormLength(int firstLengthByte) { + return firstLengthByte & 0x7f; + } + + private static int readLongFormLength(InputStream in, int firstLengthByte) + throws IOException, BerDataValueFormatException { + // The low 7 bits of the first byte represent the number of bytes (following the first + // byte) in which the length is in big-endian base-256 form + int byteCount = firstLengthByte & 0x7f; + if (byteCount > 4) { + throw new BerDataValueFormatException("Length too large: " + byteCount + " bytes"); + } + int result = 0; + for (int i = 0; i < byteCount; i++) { + int b = in.read(); + if (b == -1) { + throw new BerDataValueFormatException("Truncated length"); + } + if (result > Integer.MAX_VALUE >>> 8) { + throw new BerDataValueFormatException("Length too large"); + } + result <<= 8; + result |= b & 0xff; + } + return result; + } + + private static void skipDefiniteLengthContents(InputStream in, int len) + throws IOException, BerDataValueFormatException { + long bytesRead = 0; + while (len > 0) { + int skipped = (int) in.skip(len); + if (skipped <= 0) { + throw new BerDataValueFormatException( + "Truncated definite-length contents: " + bytesRead + " bytes read" + + ", " + len + " missing"); + } + len -= skipped; + bytesRead += skipped; + } + } + + private static int skipPrimitiveIndefiniteLengthContents(InputStream in) + throws IOException, BerDataValueFormatException { + // Contents are terminated by 0x00 0x00 + boolean prevZeroByte = false; + int bytesRead = 0; + while (true) { + int b = in.read(); + if (b == -1) { + throw new BerDataValueFormatException( + "Truncated indefinite-length contents: " + bytesRead + " bytes read"); + } + bytesRead++; + if (bytesRead < 0) { + throw new BerDataValueFormatException("Indefinite-length contents too long"); + } + if (b == 0) { + if (prevZeroByte) { + // End of contents reached -- we've read the value and its terminator 0x00 0x00 + return bytesRead - 2; + } + prevZeroByte = true; + continue; + } else { + prevZeroByte = false; + } + } + } + + private static int skipConstructedIndefiniteLengthContents(RecordingInputStream in) + throws BerDataValueFormatException { + // Contents are terminated by 0x00 0x00. However, this data value is constructed, meaning it + // can contain data values which are indefinite length encoded as well. As a result, we + // must parse the direct children of this data value to correctly skip over the contents of + // this data value. + int readByteCountBefore = in.getReadByteCount(); + while (true) { + // We can't easily peek for the 0x00 0x00 terminator using the provided InputStream. + // Thus, we use the fact that 0x00 0x00 parses as a data value whose encoded form we + // then check below to see whether it's 0x00 0x00. + BerDataValue dataValue = readDataValue(in); + if (dataValue == null) { + throw new BerDataValueFormatException( + "Truncated indefinite-length contents: " + + (in.getReadByteCount() - readByteCountBefore) + " bytes read"); + } + if (in.getReadByteCount() <= 0) { + throw new BerDataValueFormatException("Indefinite-length contents too long"); + } + ByteBuffer encoded = dataValue.getEncoded(); + if ((encoded.remaining() == 2) && (encoded.get(0) == 0) && (encoded.get(1) == 0)) { + // 0x00 0x00 encountered + return in.getReadByteCount() - readByteCountBefore - 2; + } + } + } + + private static class RecordingInputStream extends InputStream { + private final InputStream mIn; + private final ByteArrayOutputStream mBuf; + + private RecordingInputStream(InputStream in) { + mIn = in; + mBuf = new ByteArrayOutputStream(); + } + + public byte[] getReadBytes() { + return mBuf.toByteArray(); + } + + public int getReadByteCount() { + return mBuf.size(); + } + + @Override + public int read() throws IOException { + int b = mIn.read(); + if (b != -1) { + mBuf.write(b); + } + return b; + } + + @Override + public int read(byte[] b) throws IOException { + int len = mIn.read(b); + if (len > 0) { + mBuf.write(b, 0, len); + } + return len; + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + len = mIn.read(b, off, len); + if (len > 0) { + mBuf.write(b, off, len); + } + return len; + } + + @Override + public long skip(long n) throws IOException { + if (n <= 0) { + return mIn.skip(n); + } + + byte[] buf = new byte[4096]; + int len = mIn.read(buf, 0, (int) Math.min(buf.length, n)); + if (len > 0) { + mBuf.write(buf, 0, len); + } + return (len < 0) ? 0 : len; + } + + @Override + public int available() throws IOException { + return super.available(); + } + + @Override + public void close() throws IOException { + super.close(); + } + + @Override + public synchronized void mark(int readlimit) {} + + @Override + public synchronized void reset() throws IOException { + throw new IOException("mark/reset not supported"); + } + + @Override + public boolean markSupported() { + return false; + } + } +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/jar/ManifestParser.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/jar/ManifestParser.java new file mode 100644 index 0000000000..ab0a5dad8a --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/jar/ManifestParser.java @@ -0,0 +1,363 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.jar; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.jar.Attributes; + +/** + * JAR manifest and signature file parser. + * + * <p>These files consist of a main section followed by individual sections. Individual sections + * are named, their names referring to JAR entries. + * + * @see <a href="https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#JAR_Manifest">JAR Manifest format</a> + */ +public class ManifestParser { + + private final byte[] mManifest; + private int mOffset; + private int mEndOffset; + + private byte[] mBufferedLine; + + /** + * Constructs a new {@code ManifestParser} with the provided input. + */ + public ManifestParser(byte[] data) { + this(data, 0, data.length); + } + + /** + * Constructs a new {@code ManifestParser} with the provided input. + */ + public ManifestParser(byte[] data, int offset, int length) { + mManifest = data; + mOffset = offset; + mEndOffset = offset + length; + } + + /** + * Returns the remaining sections of this file. + */ + public List<Section> readAllSections() { + List<Section> sections = new ArrayList<>(); + Section section; + while ((section = readSection()) != null) { + sections.add(section); + } + return sections; + } + + /** + * Returns the next section from this file or {@code null} if end of file has been reached. + */ + public Section readSection() { + // Locate the first non-empty line + int sectionStartOffset; + String attr; + do { + sectionStartOffset = mOffset; + attr = readAttribute(); + if (attr == null) { + return null; + } + } while (attr.length() == 0); + List<Attribute> attrs = new ArrayList<>(); + attrs.add(parseAttr(attr)); + + // Read attributes until end of section reached + while (true) { + attr = readAttribute(); + if ((attr == null) || (attr.length() == 0)) { + // End of section + break; + } + attrs.add(parseAttr(attr)); + } + + int sectionEndOffset = mOffset; + int sectionSizeBytes = sectionEndOffset - sectionStartOffset; + + return new Section(sectionStartOffset, sectionSizeBytes, attrs); + } + + private static Attribute parseAttr(String attr) { + // Name is separated from value by a semicolon followed by a single SPACE character. + // This permits trailing spaces in names and leading and trailing spaces in values. + // Some APK obfuscators take advantage of this fact. We thus need to preserve these unusual + // spaces to be able to parse such obfuscated APKs. + int delimiterIndex = attr.indexOf(": "); + if (delimiterIndex == -1) { + return new Attribute(attr, ""); + } else { + return new Attribute( + attr.substring(0, delimiterIndex), + attr.substring(delimiterIndex + ": ".length())); + } + } + + /** + * Returns the next attribute or empty {@code String} if end of section has been reached or + * {@code null} if end of input has been reached. + */ + private String readAttribute() { + byte[] bytes = readAttributeBytes(); + if (bytes == null) { + return null; + } else if (bytes.length == 0) { + return ""; + } else { + return new String(bytes, StandardCharsets.UTF_8); + } + } + + /** + * Returns the next attribute or empty array if end of section has been reached or {@code null} + * if end of input has been reached. + */ + private byte[] readAttributeBytes() { + // Check whether end of section was reached during previous invocation + if ((mBufferedLine != null) && (mBufferedLine.length == 0)) { + mBufferedLine = null; + return EMPTY_BYTE_ARRAY; + } + + // Read the next line + byte[] line = readLine(); + if (line == null) { + // End of input + if (mBufferedLine != null) { + byte[] result = mBufferedLine; + mBufferedLine = null; + return result; + } + return null; + } + + // Consume the read line + if (line.length == 0) { + // End of section + if (mBufferedLine != null) { + byte[] result = mBufferedLine; + mBufferedLine = EMPTY_BYTE_ARRAY; + return result; + } + return EMPTY_BYTE_ARRAY; + } + byte[] attrLine; + if (mBufferedLine == null) { + attrLine = line; + } else { + if ((line.length == 0) || (line[0] != ' ')) { + // The most common case: buffered line is a full attribute + byte[] result = mBufferedLine; + mBufferedLine = line; + return result; + } + attrLine = mBufferedLine; + mBufferedLine = null; + attrLine = concat(attrLine, line, 1, line.length - 1); + } + + // Everything's buffered in attrLine now. mBufferedLine is null + + // Read more lines + while (true) { + line = readLine(); + if (line == null) { + // End of input + return attrLine; + } else if (line.length == 0) { + // End of section + mBufferedLine = EMPTY_BYTE_ARRAY; // return "end of section" next time + return attrLine; + } + if (line[0] == ' ') { + // Continuation line + attrLine = concat(attrLine, line, 1, line.length - 1); + } else { + // Next attribute + mBufferedLine = line; + return attrLine; + } + } + } + + private static final byte[] EMPTY_BYTE_ARRAY = new byte[0]; + + private static byte[] concat(byte[] arr1, byte[] arr2, int offset2, int length2) { + byte[] result = new byte[arr1.length + length2]; + System.arraycopy(arr1, 0, result, 0, arr1.length); + System.arraycopy(arr2, offset2, result, arr1.length, length2); + return result; + } + + /** + * Returns the next line (without line delimiter characters) or {@code null} if end of input has + * been reached. + */ + private byte[] readLine() { + if (mOffset >= mEndOffset) { + return null; + } + int startOffset = mOffset; + int newlineStartOffset = -1; + int newlineEndOffset = -1; + for (int i = startOffset; i < mEndOffset; i++) { + byte b = mManifest[i]; + if (b == '\r') { + newlineStartOffset = i; + int nextIndex = i + 1; + if ((nextIndex < mEndOffset) && (mManifest[nextIndex] == '\n')) { + newlineEndOffset = nextIndex + 1; + break; + } + newlineEndOffset = nextIndex; + break; + } else if (b == '\n') { + newlineStartOffset = i; + newlineEndOffset = i + 1; + break; + } + } + if (newlineStartOffset == -1) { + newlineStartOffset = mEndOffset; + newlineEndOffset = mEndOffset; + } + mOffset = newlineEndOffset; + + if (newlineStartOffset == startOffset) { + return EMPTY_BYTE_ARRAY; + } + return Arrays.copyOfRange(mManifest, startOffset, newlineStartOffset); + } + + + /** + * Attribute. + */ + public static class Attribute { + private final String mName; + private final String mValue; + + /** + * Constructs a new {@code Attribute} with the provided name and value. + */ + public Attribute(String name, String value) { + mName = name; + mValue = value; + } + + /** + * Returns this attribute's name. + */ + public String getName() { + return mName; + } + + /** + * Returns this attribute's value. + */ + public String getValue() { + return mValue; + } + } + + /** + * Section. + */ + public static class Section { + private final int mStartOffset; + private final int mSizeBytes; + private final String mName; + private final List<Attribute> mAttributes; + + /** + * Constructs a new {@code Section}. + * + * @param startOffset start offset (in bytes) of the section in the input file + * @param sizeBytes size (in bytes) of the section in the input file + * @param attrs attributes contained in the section + */ + public Section(int startOffset, int sizeBytes, List<Attribute> attrs) { + mStartOffset = startOffset; + mSizeBytes = sizeBytes; + String sectionName = null; + if (!attrs.isEmpty()) { + Attribute firstAttr = attrs.get(0); + if ("Name".equalsIgnoreCase(firstAttr.getName())) { + sectionName = firstAttr.getValue(); + } + } + mName = sectionName; + mAttributes = Collections.unmodifiableList(new ArrayList<>(attrs)); + } + + public String getName() { + return mName; + } + + /** + * Returns the offset (in bytes) at which this section starts in the input. + */ + public int getStartOffset() { + return mStartOffset; + } + + /** + * Returns the size (in bytes) of this section in the input. + */ + public int getSizeBytes() { + return mSizeBytes; + } + + /** + * Returns this section's attributes, in the order in which they appear in the input. + */ + public List<Attribute> getAttributes() { + return mAttributes; + } + + /** + * Returns the value of the specified attribute in this section or {@code null} if this + * section does not contain a matching attribute. + */ + public String getAttributeValue(Attributes.Name name) { + return getAttributeValue(name.toString()); + } + + /** + * Returns the value of the specified attribute in this section or {@code null} if this + * section does not contain a matching attribute. + * + * @param name name of the attribute. Attribute names are case-insensitive. + */ + public String getAttributeValue(String name) { + for (Attribute attr : mAttributes) { + if (attr.getName().equalsIgnoreCase(name)) { + return attr.getValue(); + } + } + return null; + } + } +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/jar/ManifestWriter.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/jar/ManifestWriter.java new file mode 100644 index 0000000000..fa01beb7b7 --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/jar/ManifestWriter.java @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.jar; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.Set; +import java.util.SortedMap; +import java.util.TreeMap; +import java.util.jar.Attributes; + +/** + * Producer of {@code META-INF/MANIFEST.MF} file. + * + * @see <a href="https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#JAR_Manifest">JAR Manifest format</a> + */ +public abstract class ManifestWriter { + + private static final byte[] CRLF = new byte[] {'\r', '\n'}; + private static final int MAX_LINE_LENGTH = 70; + + private ManifestWriter() {} + + public static void writeMainSection(OutputStream out, Attributes attributes) + throws IOException { + + // Main section must start with the Manifest-Version attribute. + // See https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File. + String manifestVersion = attributes.getValue(Attributes.Name.MANIFEST_VERSION); + if (manifestVersion == null) { + throw new IllegalArgumentException( + "Mandatory " + Attributes.Name.MANIFEST_VERSION + " attribute missing"); + } + writeAttribute(out, Attributes.Name.MANIFEST_VERSION, manifestVersion); + + if (attributes.size() > 1) { + SortedMap<String, String> namedAttributes = getAttributesSortedByName(attributes); + namedAttributes.remove(Attributes.Name.MANIFEST_VERSION.toString()); + writeAttributes(out, namedAttributes); + } + writeSectionDelimiter(out); + } + + public static void writeIndividualSection(OutputStream out, String name, Attributes attributes) + throws IOException { + writeAttribute(out, "Name", name); + + if (!attributes.isEmpty()) { + writeAttributes(out, getAttributesSortedByName(attributes)); + } + writeSectionDelimiter(out); + } + + static void writeSectionDelimiter(OutputStream out) throws IOException { + out.write(CRLF); + } + + static void writeAttribute(OutputStream out, Attributes.Name name, String value) + throws IOException { + writeAttribute(out, name.toString(), value); + } + + private static void writeAttribute(OutputStream out, String name, String value) + throws IOException { + writeLine(out, name + ": " + value); + } + + private static void writeLine(OutputStream out, String line) throws IOException { + byte[] lineBytes = line.getBytes(StandardCharsets.UTF_8); + int offset = 0; + int remaining = lineBytes.length; + boolean firstLine = true; + while (remaining > 0) { + int chunkLength; + if (firstLine) { + // First line + chunkLength = Math.min(remaining, MAX_LINE_LENGTH); + } else { + // Continuation line + out.write(CRLF); + out.write(' '); + chunkLength = Math.min(remaining, MAX_LINE_LENGTH - 1); + } + out.write(lineBytes, offset, chunkLength); + offset += chunkLength; + remaining -= chunkLength; + firstLine = false; + } + out.write(CRLF); + } + + static SortedMap<String, String> getAttributesSortedByName(Attributes attributes) { + Set<Map.Entry<Object, Object>> attributesEntries = attributes.entrySet(); + SortedMap<String, String> namedAttributes = new TreeMap<String, String>(); + for (Map.Entry<Object, Object> attribute : attributesEntries) { + String attrName = attribute.getKey().toString(); + String attrValue = attribute.getValue().toString(); + namedAttributes.put(attrName, attrValue); + } + return namedAttributes; + } + + static void writeAttributes( + OutputStream out, SortedMap<String, String> attributesSortedByName) throws IOException { + for (Map.Entry<String, String> attribute : attributesSortedByName.entrySet()) { + String attrName = attribute.getKey(); + String attrValue = attribute.getValue(); + writeAttribute(out, attrName, attrValue); + } + } +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/jar/SignatureFileWriter.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/jar/SignatureFileWriter.java new file mode 100644 index 0000000000..fd8cbff8dc --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/jar/SignatureFileWriter.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.jar; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.SortedMap; +import java.util.jar.Attributes; + +/** + * Producer of JAR signature file ({@code *.SF}). + * + * @see <a href="https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#JAR_Manifest">JAR Manifest format</a> + */ +public abstract class SignatureFileWriter { + private SignatureFileWriter() {} + + public static void writeMainSection(OutputStream out, Attributes attributes) + throws IOException { + + // Main section must start with the Signature-Version attribute. + // See https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File. + String signatureVersion = attributes.getValue(Attributes.Name.SIGNATURE_VERSION); + if (signatureVersion == null) { + throw new IllegalArgumentException( + "Mandatory " + Attributes.Name.SIGNATURE_VERSION + " attribute missing"); + } + ManifestWriter.writeAttribute(out, Attributes.Name.SIGNATURE_VERSION, signatureVersion); + + if (attributes.size() > 1) { + SortedMap<String, String> namedAttributes = + ManifestWriter.getAttributesSortedByName(attributes); + namedAttributes.remove(Attributes.Name.SIGNATURE_VERSION.toString()); + ManifestWriter.writeAttributes(out, namedAttributes); + } + writeSectionDelimiter(out); + } + + public static void writeIndividualSection(OutputStream out, String name, Attributes attributes) + throws IOException { + ManifestWriter.writeIndividualSection(out, name, attributes); + } + + public static void writeSectionDelimiter(OutputStream out) throws IOException { + ManifestWriter.writeSectionDelimiter(out); + } +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/oid/OidConstants.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/oid/OidConstants.java new file mode 100644 index 0000000000..d80cbaa6ee --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/oid/OidConstants.java @@ -0,0 +1,463 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.oid; + +import com.android.apksig.internal.util.InclusiveIntRange; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class OidConstants { + public static final String OID_DIGEST_MD5 = "1.2.840.113549.2.5"; + public static final String OID_DIGEST_SHA1 = "1.3.14.3.2.26"; + public static final String OID_DIGEST_SHA224 = "2.16.840.1.101.3.4.2.4"; + public static final String OID_DIGEST_SHA256 = "2.16.840.1.101.3.4.2.1"; + public static final String OID_DIGEST_SHA384 = "2.16.840.1.101.3.4.2.2"; + public static final String OID_DIGEST_SHA512 = "2.16.840.1.101.3.4.2.3"; + + public static final String OID_SIG_RSA = "1.2.840.113549.1.1.1"; + public static final String OID_SIG_MD5_WITH_RSA = "1.2.840.113549.1.1.4"; + public static final String OID_SIG_SHA1_WITH_RSA = "1.2.840.113549.1.1.5"; + public static final String OID_SIG_SHA224_WITH_RSA = "1.2.840.113549.1.1.14"; + public static final String OID_SIG_SHA256_WITH_RSA = "1.2.840.113549.1.1.11"; + public static final String OID_SIG_SHA384_WITH_RSA = "1.2.840.113549.1.1.12"; + public static final String OID_SIG_SHA512_WITH_RSA = "1.2.840.113549.1.1.13"; + + public static final String OID_SIG_DSA = "1.2.840.10040.4.1"; + public static final String OID_SIG_SHA1_WITH_DSA = "1.2.840.10040.4.3"; + public static final String OID_SIG_SHA224_WITH_DSA = "2.16.840.1.101.3.4.3.1"; + public static final String OID_SIG_SHA256_WITH_DSA = "2.16.840.1.101.3.4.3.2"; + public static final String OID_SIG_SHA384_WITH_DSA = "2.16.840.1.101.3.4.3.3"; + public static final String OID_SIG_SHA512_WITH_DSA = "2.16.840.1.101.3.4.3.4"; + + public static final String OID_SIG_EC_PUBLIC_KEY = "1.2.840.10045.2.1"; + public static final String OID_SIG_SHA1_WITH_ECDSA = "1.2.840.10045.4.1"; + public static final String OID_SIG_SHA224_WITH_ECDSA = "1.2.840.10045.4.3.1"; + public static final String OID_SIG_SHA256_WITH_ECDSA = "1.2.840.10045.4.3.2"; + public static final String OID_SIG_SHA384_WITH_ECDSA = "1.2.840.10045.4.3.3"; + public static final String OID_SIG_SHA512_WITH_ECDSA = "1.2.840.10045.4.3.4"; + + public static final Map<String, List<InclusiveIntRange>> SUPPORTED_SIG_ALG_OIDS = + new HashMap<>(); + static { + addSupportedSigAlg( + OID_DIGEST_MD5, OID_SIG_RSA, + InclusiveIntRange.from(0)); + addSupportedSigAlg( + OID_DIGEST_MD5, OID_SIG_MD5_WITH_RSA, + InclusiveIntRange.fromTo(0, 8), InclusiveIntRange.from(21)); + addSupportedSigAlg( + OID_DIGEST_MD5, OID_SIG_SHA1_WITH_RSA, + InclusiveIntRange.fromTo(21, 23)); + addSupportedSigAlg( + OID_DIGEST_MD5, OID_SIG_SHA224_WITH_RSA, + InclusiveIntRange.fromTo(21, 23)); + addSupportedSigAlg( + OID_DIGEST_MD5, OID_SIG_SHA256_WITH_RSA, + InclusiveIntRange.fromTo(21, 23)); + addSupportedSigAlg( + OID_DIGEST_MD5, OID_SIG_SHA384_WITH_RSA, + InclusiveIntRange.fromTo(21, 23)); + addSupportedSigAlg( + OID_DIGEST_MD5, OID_SIG_SHA512_WITH_RSA, + InclusiveIntRange.fromTo(21, 23)); + + addSupportedSigAlg( + OID_DIGEST_SHA1, OID_SIG_RSA, + InclusiveIntRange.from(0)); + addSupportedSigAlg( + OID_DIGEST_SHA1, OID_SIG_MD5_WITH_RSA, + InclusiveIntRange.fromTo(21, 23)); + addSupportedSigAlg( + OID_DIGEST_SHA1, OID_SIG_SHA1_WITH_RSA, + InclusiveIntRange.from(0)); + addSupportedSigAlg( + OID_DIGEST_SHA1, OID_SIG_SHA224_WITH_RSA, + InclusiveIntRange.fromTo(21, 23)); + addSupportedSigAlg( + OID_DIGEST_SHA1, OID_SIG_SHA256_WITH_RSA, + InclusiveIntRange.fromTo(21, 23)); + addSupportedSigAlg( + OID_DIGEST_SHA1, OID_SIG_SHA384_WITH_RSA, + InclusiveIntRange.fromTo(21, 23)); + addSupportedSigAlg( + OID_DIGEST_SHA1, OID_SIG_SHA512_WITH_RSA, + InclusiveIntRange.fromTo(21, 23)); + + addSupportedSigAlg( + OID_DIGEST_SHA224, OID_SIG_RSA, + InclusiveIntRange.fromTo(0, 8), InclusiveIntRange.from(21)); + addSupportedSigAlg( + OID_DIGEST_SHA224, OID_SIG_MD5_WITH_RSA, + InclusiveIntRange.fromTo(21, 23)); + addSupportedSigAlg( + OID_DIGEST_SHA224, OID_SIG_SHA1_WITH_RSA, + InclusiveIntRange.fromTo(21, 23)); + addSupportedSigAlg( + OID_DIGEST_SHA224, OID_SIG_SHA224_WITH_RSA, + InclusiveIntRange.fromTo(0, 8), InclusiveIntRange.from(21)); + addSupportedSigAlg( + OID_DIGEST_SHA224, OID_SIG_SHA256_WITH_RSA, + InclusiveIntRange.fromTo(21, 21)); + addSupportedSigAlg( + OID_DIGEST_SHA224, OID_SIG_SHA384_WITH_RSA, + InclusiveIntRange.fromTo(21, 23)); + addSupportedSigAlg( + OID_DIGEST_SHA224, OID_SIG_SHA512_WITH_RSA, + InclusiveIntRange.fromTo(21, 23)); + + addSupportedSigAlg( + OID_DIGEST_SHA256, OID_SIG_RSA, + InclusiveIntRange.fromTo(0, 8), InclusiveIntRange.from(18)); + addSupportedSigAlg( + OID_DIGEST_SHA256, OID_SIG_MD5_WITH_RSA, + InclusiveIntRange.fromTo(21, 23)); + addSupportedSigAlg( + OID_DIGEST_SHA256, OID_SIG_SHA1_WITH_RSA, + InclusiveIntRange.fromTo(21, 21)); + addSupportedSigAlg( + OID_DIGEST_SHA256, OID_SIG_SHA224_WITH_RSA, + InclusiveIntRange.fromTo(21, 23)); + addSupportedSigAlg( + OID_DIGEST_SHA256, OID_SIG_SHA256_WITH_RSA, + InclusiveIntRange.fromTo(0, 8), InclusiveIntRange.from(18)); + addSupportedSigAlg( + OID_DIGEST_SHA256, OID_SIG_SHA384_WITH_RSA, + InclusiveIntRange.fromTo(21, 23)); + addSupportedSigAlg( + OID_DIGEST_SHA256, OID_SIG_SHA512_WITH_RSA, + InclusiveIntRange.fromTo(21, 23)); + + addSupportedSigAlg( + OID_DIGEST_SHA384, OID_SIG_RSA, + InclusiveIntRange.from(18)); + addSupportedSigAlg( + OID_DIGEST_SHA384, OID_SIG_MD5_WITH_RSA, + InclusiveIntRange.fromTo(21, 23)); + addSupportedSigAlg( + OID_DIGEST_SHA384, OID_SIG_SHA1_WITH_RSA, + InclusiveIntRange.fromTo(21, 23)); + addSupportedSigAlg( + OID_DIGEST_SHA384, OID_SIG_SHA224_WITH_RSA, + InclusiveIntRange.fromTo(21, 23)); + addSupportedSigAlg( + OID_DIGEST_SHA384, OID_SIG_SHA256_WITH_RSA, + InclusiveIntRange.fromTo(21, 23)); + addSupportedSigAlg( + OID_DIGEST_SHA384, OID_SIG_SHA384_WITH_RSA, + InclusiveIntRange.from(21)); + addSupportedSigAlg( + OID_DIGEST_SHA384, OID_SIG_SHA512_WITH_RSA, + InclusiveIntRange.fromTo(21, 23)); + + addSupportedSigAlg( + OID_DIGEST_SHA512, OID_SIG_RSA, + InclusiveIntRange.from(18)); + addSupportedSigAlg( + OID_DIGEST_SHA512, OID_SIG_MD5_WITH_RSA, + InclusiveIntRange.fromTo(21, 23)); + addSupportedSigAlg( + OID_DIGEST_SHA512, OID_SIG_SHA1_WITH_RSA, + InclusiveIntRange.fromTo(21, 23)); + addSupportedSigAlg( + OID_DIGEST_SHA512, OID_SIG_SHA224_WITH_RSA, + InclusiveIntRange.fromTo(21, 23)); + addSupportedSigAlg( + OID_DIGEST_SHA512, OID_SIG_SHA256_WITH_RSA, + InclusiveIntRange.fromTo(21, 23)); + addSupportedSigAlg( + OID_DIGEST_SHA512, OID_SIG_SHA384_WITH_RSA, + InclusiveIntRange.fromTo(21, 21)); + addSupportedSigAlg( + OID_DIGEST_SHA512, OID_SIG_SHA512_WITH_RSA, + InclusiveIntRange.from(21)); + + addSupportedSigAlg( + OID_DIGEST_MD5, OID_SIG_SHA1_WITH_DSA, + InclusiveIntRange.fromTo(21, 23)); + addSupportedSigAlg( + OID_DIGEST_MD5, OID_SIG_SHA224_WITH_DSA, + InclusiveIntRange.fromTo(21, 23)); + addSupportedSigAlg( + OID_DIGEST_MD5, OID_SIG_SHA256_WITH_DSA, + InclusiveIntRange.fromTo(21, 23)); + + addSupportedSigAlg( + OID_DIGEST_SHA1, OID_SIG_DSA, + InclusiveIntRange.from(0)); + addSupportedSigAlg( + OID_DIGEST_SHA1, OID_SIG_SHA1_WITH_DSA, + InclusiveIntRange.from(9)); + addSupportedSigAlg( + OID_DIGEST_SHA1, OID_SIG_SHA224_WITH_DSA, + InclusiveIntRange.fromTo(21, 23)); + addSupportedSigAlg( + OID_DIGEST_SHA1, OID_SIG_SHA256_WITH_DSA, + InclusiveIntRange.fromTo(21, 23)); + + addSupportedSigAlg( + OID_DIGEST_SHA224, OID_SIG_DSA, + InclusiveIntRange.from(22)); + addSupportedSigAlg( + OID_DIGEST_SHA224, OID_SIG_SHA1_WITH_DSA, + InclusiveIntRange.fromTo(21, 23)); + addSupportedSigAlg( + OID_DIGEST_SHA224, OID_SIG_SHA224_WITH_DSA, + InclusiveIntRange.from(21)); + addSupportedSigAlg( + OID_DIGEST_SHA224, OID_SIG_SHA256_WITH_DSA, + InclusiveIntRange.fromTo(21, 23)); + + addSupportedSigAlg( + OID_DIGEST_SHA256, OID_SIG_DSA, + InclusiveIntRange.from(22)); + addSupportedSigAlg( + OID_DIGEST_SHA256, OID_SIG_SHA1_WITH_DSA, + InclusiveIntRange.fromTo(21, 23)); + addSupportedSigAlg( + OID_DIGEST_SHA256, OID_SIG_SHA224_WITH_DSA, + InclusiveIntRange.fromTo(21, 23)); + addSupportedSigAlg( + OID_DIGEST_SHA256, OID_SIG_SHA256_WITH_DSA, + InclusiveIntRange.from(21)); + + addSupportedSigAlg( + OID_DIGEST_SHA384, OID_SIG_SHA1_WITH_DSA, + InclusiveIntRange.fromTo(21, 23)); + addSupportedSigAlg( + OID_DIGEST_SHA384, OID_SIG_SHA224_WITH_DSA, + InclusiveIntRange.fromTo(21, 23)); + addSupportedSigAlg( + OID_DIGEST_SHA384, OID_SIG_SHA256_WITH_DSA, + InclusiveIntRange.fromTo(21, 23)); + + addSupportedSigAlg( + OID_DIGEST_SHA512, OID_SIG_SHA1_WITH_DSA, + InclusiveIntRange.fromTo(21, 23)); + addSupportedSigAlg( + OID_DIGEST_SHA512, OID_SIG_SHA224_WITH_DSA, + InclusiveIntRange.fromTo(21, 23)); + addSupportedSigAlg( + OID_DIGEST_SHA512, OID_SIG_SHA256_WITH_DSA, + InclusiveIntRange.fromTo(21, 23)); + + addSupportedSigAlg( + OID_DIGEST_SHA1, OID_SIG_EC_PUBLIC_KEY, + InclusiveIntRange.from(18)); + addSupportedSigAlg( + OID_DIGEST_SHA224, OID_SIG_EC_PUBLIC_KEY, + InclusiveIntRange.from(21)); + addSupportedSigAlg( + OID_DIGEST_SHA256, OID_SIG_EC_PUBLIC_KEY, + InclusiveIntRange.from(18)); + addSupportedSigAlg( + OID_DIGEST_SHA384, OID_SIG_EC_PUBLIC_KEY, + InclusiveIntRange.from(18)); + addSupportedSigAlg( + OID_DIGEST_SHA512, OID_SIG_EC_PUBLIC_KEY, + InclusiveIntRange.from(18)); + + addSupportedSigAlg( + OID_DIGEST_MD5, OID_SIG_SHA1_WITH_ECDSA, + InclusiveIntRange.fromTo(21, 23)); + addSupportedSigAlg( + OID_DIGEST_MD5, OID_SIG_SHA224_WITH_ECDSA, + InclusiveIntRange.fromTo(21, 23)); + addSupportedSigAlg( + OID_DIGEST_MD5, OID_SIG_SHA256_WITH_ECDSA, + InclusiveIntRange.fromTo(21, 23)); + addSupportedSigAlg( + OID_DIGEST_MD5, OID_SIG_SHA384_WITH_ECDSA, + InclusiveIntRange.fromTo(21, 23)); + addSupportedSigAlg( + OID_DIGEST_MD5, OID_SIG_SHA512_WITH_ECDSA, + InclusiveIntRange.fromTo(21, 23)); + + addSupportedSigAlg( + OID_DIGEST_SHA1, OID_SIG_SHA1_WITH_ECDSA, + InclusiveIntRange.from(18)); + addSupportedSigAlg( + OID_DIGEST_SHA1, OID_SIG_SHA224_WITH_ECDSA, + InclusiveIntRange.fromTo(21, 23)); + addSupportedSigAlg( + OID_DIGEST_SHA1, OID_SIG_SHA256_WITH_ECDSA, + InclusiveIntRange.fromTo(21, 23)); + addSupportedSigAlg( + OID_DIGEST_SHA1, OID_SIG_SHA384_WITH_ECDSA, + InclusiveIntRange.fromTo(21, 23)); + addSupportedSigAlg( + OID_DIGEST_SHA1, OID_SIG_SHA512_WITH_ECDSA, + InclusiveIntRange.fromTo(21, 23)); + + addSupportedSigAlg( + OID_DIGEST_SHA224, OID_SIG_SHA1_WITH_ECDSA, + InclusiveIntRange.fromTo(21, 23)); + addSupportedSigAlg( + OID_DIGEST_SHA224, OID_SIG_SHA224_WITH_ECDSA, + InclusiveIntRange.from(21)); + addSupportedSigAlg( + OID_DIGEST_SHA224, OID_SIG_SHA256_WITH_ECDSA, + InclusiveIntRange.fromTo(21, 23)); + addSupportedSigAlg( + OID_DIGEST_SHA224, OID_SIG_SHA384_WITH_ECDSA, + InclusiveIntRange.fromTo(21, 23)); + addSupportedSigAlg( + OID_DIGEST_SHA224, OID_SIG_SHA512_WITH_ECDSA, + InclusiveIntRange.fromTo(21, 23)); + + addSupportedSigAlg( + OID_DIGEST_SHA256, OID_SIG_SHA1_WITH_ECDSA, + InclusiveIntRange.fromTo(21, 23)); + addSupportedSigAlg( + OID_DIGEST_SHA256, OID_SIG_SHA224_WITH_ECDSA, + InclusiveIntRange.fromTo(21, 23)); + addSupportedSigAlg( + OID_DIGEST_SHA256, OID_SIG_SHA256_WITH_ECDSA, + InclusiveIntRange.from(21)); + addSupportedSigAlg( + OID_DIGEST_SHA256, OID_SIG_SHA384_WITH_ECDSA, + InclusiveIntRange.fromTo(21, 23)); + addSupportedSigAlg( + OID_DIGEST_SHA256, OID_SIG_SHA512_WITH_ECDSA, + InclusiveIntRange.fromTo(21, 23)); + + addSupportedSigAlg( + OID_DIGEST_SHA384, OID_SIG_SHA1_WITH_ECDSA, + InclusiveIntRange.fromTo(21, 23)); + addSupportedSigAlg( + OID_DIGEST_SHA384, OID_SIG_SHA224_WITH_ECDSA, + InclusiveIntRange.fromTo(21, 23)); + addSupportedSigAlg( + OID_DIGEST_SHA384, OID_SIG_SHA256_WITH_ECDSA, + InclusiveIntRange.fromTo(21, 23)); + addSupportedSigAlg( + OID_DIGEST_SHA384, OID_SIG_SHA384_WITH_ECDSA, + InclusiveIntRange.from(21)); + addSupportedSigAlg( + OID_DIGEST_SHA384, OID_SIG_SHA512_WITH_ECDSA, + InclusiveIntRange.fromTo(21, 23)); + + addSupportedSigAlg( + OID_DIGEST_SHA512, OID_SIG_SHA1_WITH_ECDSA, + InclusiveIntRange.fromTo(21, 23)); + addSupportedSigAlg( + OID_DIGEST_SHA512, OID_SIG_SHA224_WITH_ECDSA, + InclusiveIntRange.fromTo(21, 23)); + addSupportedSigAlg( + OID_DIGEST_SHA512, OID_SIG_SHA256_WITH_ECDSA, + InclusiveIntRange.fromTo(21, 23)); + addSupportedSigAlg( + OID_DIGEST_SHA512, OID_SIG_SHA384_WITH_ECDSA, + InclusiveIntRange.fromTo(21, 23)); + addSupportedSigAlg( + OID_DIGEST_SHA512, OID_SIG_SHA512_WITH_ECDSA, + InclusiveIntRange.from(21)); + } + + public static void addSupportedSigAlg( + String digestAlgorithmOid, + String signatureAlgorithmOid, + InclusiveIntRange... supportedApiLevels) { + SUPPORTED_SIG_ALG_OIDS.put( + digestAlgorithmOid + "with" + signatureAlgorithmOid, + Arrays.asList(supportedApiLevels)); + } + + public static List<InclusiveIntRange> getSigAlgSupportedApiLevels( + String digestAlgorithmOid, + String signatureAlgorithmOid) { + List<InclusiveIntRange> result = + SUPPORTED_SIG_ALG_OIDS.get(digestAlgorithmOid + "with" + signatureAlgorithmOid); + return (result != null) ? result : Collections.emptyList(); + } + + public static class OidToUserFriendlyNameMapper { + private OidToUserFriendlyNameMapper() {} + + private static final Map<String, String> OID_TO_USER_FRIENDLY_NAME = new HashMap<>(); + static { + OID_TO_USER_FRIENDLY_NAME.put(OID_DIGEST_MD5, "MD5"); + OID_TO_USER_FRIENDLY_NAME.put(OID_DIGEST_SHA1, "SHA-1"); + OID_TO_USER_FRIENDLY_NAME.put(OID_DIGEST_SHA224, "SHA-224"); + OID_TO_USER_FRIENDLY_NAME.put(OID_DIGEST_SHA256, "SHA-256"); + OID_TO_USER_FRIENDLY_NAME.put(OID_DIGEST_SHA384, "SHA-384"); + OID_TO_USER_FRIENDLY_NAME.put(OID_DIGEST_SHA512, "SHA-512"); + + OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_RSA, "RSA"); + OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_MD5_WITH_RSA, "MD5 with RSA"); + OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA1_WITH_RSA, "SHA-1 with RSA"); + OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA224_WITH_RSA, "SHA-224 with RSA"); + OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA256_WITH_RSA, "SHA-256 with RSA"); + OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA384_WITH_RSA, "SHA-384 with RSA"); + OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA512_WITH_RSA, "SHA-512 with RSA"); + + + OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_DSA, "DSA"); + OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA1_WITH_DSA, "SHA-1 with DSA"); + OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA224_WITH_DSA, "SHA-224 with DSA"); + OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA256_WITH_DSA, "SHA-256 with DSA"); + OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA384_WITH_DSA, "SHA-384 with DSA"); + OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA512_WITH_DSA, "SHA-512 with DSA"); + + OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_EC_PUBLIC_KEY, "ECDSA"); + OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA1_WITH_ECDSA, "SHA-1 with ECDSA"); + OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA224_WITH_ECDSA, "SHA-224 with ECDSA"); + OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA256_WITH_ECDSA, "SHA-256 with ECDSA"); + OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA384_WITH_ECDSA, "SHA-384 with ECDSA"); + OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA512_WITH_ECDSA, "SHA-512 with ECDSA"); + } + + public static String getUserFriendlyNameForOid(String oid) { + return OID_TO_USER_FRIENDLY_NAME.get(oid); + } + } + + public static final Map<String, String> OID_TO_JCA_DIGEST_ALG = new HashMap<>(); + static { + OID_TO_JCA_DIGEST_ALG.put(OID_DIGEST_MD5, "MD5"); + OID_TO_JCA_DIGEST_ALG.put(OID_DIGEST_SHA1, "SHA-1"); + OID_TO_JCA_DIGEST_ALG.put(OID_DIGEST_SHA224, "SHA-224"); + OID_TO_JCA_DIGEST_ALG.put(OID_DIGEST_SHA256, "SHA-256"); + OID_TO_JCA_DIGEST_ALG.put(OID_DIGEST_SHA384, "SHA-384"); + OID_TO_JCA_DIGEST_ALG.put(OID_DIGEST_SHA512, "SHA-512"); + } + + public static final Map<String, String> OID_TO_JCA_SIGNATURE_ALG = new HashMap<>(); + static { + OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_MD5_WITH_RSA, "MD5withRSA"); + OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA1_WITH_RSA, "SHA1withRSA"); + OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA224_WITH_RSA, "SHA224withRSA"); + OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA256_WITH_RSA, "SHA256withRSA"); + OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA384_WITH_RSA, "SHA384withRSA"); + OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA512_WITH_RSA, "SHA512withRSA"); + + OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA1_WITH_DSA, "SHA1withDSA"); + OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA224_WITH_DSA, "SHA224withDSA"); + OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA256_WITH_DSA, "SHA256withDSA"); + + OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA1_WITH_ECDSA, "SHA1withECDSA"); + OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA224_WITH_ECDSA, "SHA224withECDSA"); + OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA256_WITH_ECDSA, "SHA256withECDSA"); + OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA384_WITH_ECDSA, "SHA384withECDSA"); + OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA512_WITH_ECDSA, "SHA512withECDSA"); + } + + private OidConstants() {} +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/AlgorithmIdentifier.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/AlgorithmIdentifier.java new file mode 100644 index 0000000000..9712767293 --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/AlgorithmIdentifier.java @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.pkcs7; + +import static com.android.apksig.Constants.OID_RSA_ENCRYPTION; +import static com.android.apksig.internal.asn1.Asn1DerEncoder.ASN1_DER_NULL; +import static com.android.apksig.internal.oid.OidConstants.OID_DIGEST_SHA1; +import static com.android.apksig.internal.oid.OidConstants.OID_DIGEST_SHA256; +import static com.android.apksig.internal.oid.OidConstants.OID_SIG_DSA; +import static com.android.apksig.internal.oid.OidConstants.OID_SIG_EC_PUBLIC_KEY; +import static com.android.apksig.internal.oid.OidConstants.OID_SIG_RSA; +import static com.android.apksig.internal.oid.OidConstants.OID_SIG_SHA256_WITH_DSA; +import static com.android.apksig.internal.oid.OidConstants.OID_TO_JCA_DIGEST_ALG; +import static com.android.apksig.internal.oid.OidConstants.OID_TO_JCA_SIGNATURE_ALG; + +import com.android.apksig.internal.apk.v1.DigestAlgorithm; +import com.android.apksig.internal.asn1.Asn1Class; +import com.android.apksig.internal.asn1.Asn1Field; +import com.android.apksig.internal.asn1.Asn1OpaqueObject; +import com.android.apksig.internal.asn1.Asn1Type; +import com.android.apksig.internal.util.Pair; + +import java.security.InvalidKeyException; +import java.security.PublicKey; +import java.security.Signature; +import java.security.SignatureException; + +/** + * PKCS #7 {@code AlgorithmIdentifier} as specified in RFC 5652. + */ +@Asn1Class(type = Asn1Type.SEQUENCE) +public class AlgorithmIdentifier { + + @Asn1Field(index = 0, type = Asn1Type.OBJECT_IDENTIFIER) + public String algorithm; + + @Asn1Field(index = 1, type = Asn1Type.ANY, optional = true) + public Asn1OpaqueObject parameters; + + public AlgorithmIdentifier() {} + + public AlgorithmIdentifier(String algorithmOid, Asn1OpaqueObject parameters) { + this.algorithm = algorithmOid; + this.parameters = parameters; + } + + /** + * Returns the PKCS #7 {@code DigestAlgorithm} to use when signing using the specified digest + * algorithm. + */ + public static AlgorithmIdentifier getSignerInfoDigestAlgorithmOid( + DigestAlgorithm digestAlgorithm) { + switch (digestAlgorithm) { + case SHA1: + return new AlgorithmIdentifier(OID_DIGEST_SHA1, ASN1_DER_NULL); + case SHA256: + return new AlgorithmIdentifier(OID_DIGEST_SHA256, ASN1_DER_NULL); + } + throw new IllegalArgumentException("Unsupported digest algorithm: " + digestAlgorithm); + } + + /** + * Returns the JCA {@link Signature} algorithm and PKCS #7 {@code SignatureAlgorithm} to use + * when signing with the specified key and digest algorithm. + */ + public static Pair<String, AlgorithmIdentifier> getSignerInfoSignatureAlgorithm( + PublicKey publicKey, DigestAlgorithm digestAlgorithm, boolean deterministicDsaSigning) + throws InvalidKeyException { + String keyAlgorithm = publicKey.getAlgorithm(); + String jcaDigestPrefixForSigAlg; + switch (digestAlgorithm) { + case SHA1: + jcaDigestPrefixForSigAlg = "SHA1"; + break; + case SHA256: + jcaDigestPrefixForSigAlg = "SHA256"; + break; + default: + throw new IllegalArgumentException( + "Unexpected digest algorithm: " + digestAlgorithm); + } + if ("RSA".equalsIgnoreCase(keyAlgorithm) || OID_RSA_ENCRYPTION.equals(keyAlgorithm)) { + return Pair.of( + jcaDigestPrefixForSigAlg + "withRSA", + new AlgorithmIdentifier(OID_SIG_RSA, ASN1_DER_NULL)); + } else if ("DSA".equalsIgnoreCase(keyAlgorithm)) { + AlgorithmIdentifier sigAlgId; + switch (digestAlgorithm) { + case SHA1: + sigAlgId = + new AlgorithmIdentifier(OID_SIG_DSA, ASN1_DER_NULL); + break; + case SHA256: + // DSA signatures with SHA-256 in SignedData are accepted by Android API Level + // 21 and higher. However, there are two ways to specify their SignedData + // SignatureAlgorithm: dsaWithSha256 (2.16.840.1.101.3.4.3.2) and + // dsa (1.2.840.10040.4.1). The latter works only on API Level 22+. Thus, we use + // the former. + sigAlgId = + new AlgorithmIdentifier(OID_SIG_SHA256_WITH_DSA, ASN1_DER_NULL); + break; + default: + throw new IllegalArgumentException( + "Unexpected digest algorithm: " + digestAlgorithm); + } + String signingAlgorithmName = + jcaDigestPrefixForSigAlg + (deterministicDsaSigning ? "withDetDSA" : "withDSA"); + return Pair.of(signingAlgorithmName, sigAlgId); + } else if ("EC".equalsIgnoreCase(keyAlgorithm)) { + return Pair.of( + jcaDigestPrefixForSigAlg + "withECDSA", + new AlgorithmIdentifier(OID_SIG_EC_PUBLIC_KEY, ASN1_DER_NULL)); + } else { + throw new InvalidKeyException("Unsupported key algorithm: " + keyAlgorithm); + } + } + + public static String getJcaSignatureAlgorithm( + String digestAlgorithmOid, + String signatureAlgorithmOid) throws SignatureException { + // First check whether the signature algorithm OID alone is sufficient + String result = OID_TO_JCA_SIGNATURE_ALG.get(signatureAlgorithmOid); + if (result != null) { + return result; + } + + // Signature algorithm OID alone is insufficient. Need to combine digest algorithm OID + // with signature algorithm OID. + String suffix; + if (OID_SIG_RSA.equals(signatureAlgorithmOid)) { + suffix = "RSA"; + } else if (OID_SIG_DSA.equals(signatureAlgorithmOid)) { + suffix = "DSA"; + } else if (OID_SIG_EC_PUBLIC_KEY.equals(signatureAlgorithmOid)) { + suffix = "ECDSA"; + } else { + throw new SignatureException( + "Unsupported JCA Signature algorithm" + + " . Digest algorithm: " + digestAlgorithmOid + + ", signature algorithm: " + signatureAlgorithmOid); + } + String jcaDigestAlg = getJcaDigestAlgorithm(digestAlgorithmOid); + // Canonical name for SHA-1 with ... is SHA1with, rather than SHA1. Same for all other + // SHA algorithms. + if (jcaDigestAlg.startsWith("SHA-")) { + jcaDigestAlg = "SHA" + jcaDigestAlg.substring("SHA-".length()); + } + return jcaDigestAlg + "with" + suffix; + } + + public static String getJcaDigestAlgorithm(String oid) + throws SignatureException { + String result = OID_TO_JCA_DIGEST_ALG.get(oid); + if (result == null) { + throw new SignatureException("Unsupported digest algorithm: " + oid); + } + return result; + } +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/Attribute.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/Attribute.java new file mode 100644 index 0000000000..a6c91efac6 --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/Attribute.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.pkcs7; + +import com.android.apksig.internal.asn1.Asn1Class; +import com.android.apksig.internal.asn1.Asn1Field; +import com.android.apksig.internal.asn1.Asn1OpaqueObject; +import com.android.apksig.internal.asn1.Asn1Type; +import java.util.List; + +/** + * PKCS #7 {@code Attribute} as specified in RFC 5652. + */ +@Asn1Class(type = Asn1Type.SEQUENCE) +public class Attribute { + + @Asn1Field(index = 0, type = Asn1Type.OBJECT_IDENTIFIER) + public String attrType; + + @Asn1Field(index = 1, type = Asn1Type.SET_OF) + public List<Asn1OpaqueObject> attrValues; +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/ContentInfo.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/ContentInfo.java new file mode 100644 index 0000000000..8ab722c2db --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/ContentInfo.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.pkcs7; + +import com.android.apksig.internal.asn1.Asn1Class; +import com.android.apksig.internal.asn1.Asn1Field; +import com.android.apksig.internal.asn1.Asn1OpaqueObject; +import com.android.apksig.internal.asn1.Asn1Type; +import com.android.apksig.internal.asn1.Asn1Tagging; + +/** + * PKCS #7 {@code ContentInfo} as specified in RFC 5652. + */ +@Asn1Class(type = Asn1Type.SEQUENCE) +public class ContentInfo { + + @Asn1Field(index = 1, type = Asn1Type.OBJECT_IDENTIFIER) + public String contentType; + + @Asn1Field(index = 2, type = Asn1Type.ANY, tagging = Asn1Tagging.EXPLICIT, tagNumber = 0) + public Asn1OpaqueObject content; +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/EncapsulatedContentInfo.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/EncapsulatedContentInfo.java new file mode 100644 index 0000000000..79f41af89d --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/EncapsulatedContentInfo.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.pkcs7; + +import com.android.apksig.internal.asn1.Asn1Class; +import com.android.apksig.internal.asn1.Asn1Field; +import com.android.apksig.internal.asn1.Asn1Type; +import com.android.apksig.internal.asn1.Asn1Tagging; +import java.nio.ByteBuffer; + +/** + * PKCS #7 {@code EncapsulatedContentInfo} as specified in RFC 5652. + */ +@Asn1Class(type = Asn1Type.SEQUENCE) +public class EncapsulatedContentInfo { + + @Asn1Field(index = 0, type = Asn1Type.OBJECT_IDENTIFIER) + public String contentType; + + @Asn1Field( + index = 1, + type = Asn1Type.OCTET_STRING, + tagging = Asn1Tagging.EXPLICIT, tagNumber = 0, + optional = true) + public ByteBuffer content; + + public EncapsulatedContentInfo() {} + + public EncapsulatedContentInfo(String contentTypeOid) { + contentType = contentTypeOid; + } +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/IssuerAndSerialNumber.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/IssuerAndSerialNumber.java new file mode 100644 index 0000000000..284b11764b --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/IssuerAndSerialNumber.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.pkcs7; + +import com.android.apksig.internal.asn1.Asn1Class; +import com.android.apksig.internal.asn1.Asn1Field; +import com.android.apksig.internal.asn1.Asn1OpaqueObject; +import com.android.apksig.internal.asn1.Asn1Type; +import java.math.BigInteger; + +/** + * PKCS #7 {@code IssuerAndSerialNumber} as specified in RFC 5652. + */ +@Asn1Class(type = Asn1Type.SEQUENCE) +public class IssuerAndSerialNumber { + + @Asn1Field(index = 0, type = Asn1Type.ANY) + public Asn1OpaqueObject issuer; + + @Asn1Field(index = 1, type = Asn1Type.INTEGER) + public BigInteger certificateSerialNumber; + + public IssuerAndSerialNumber() {} + + public IssuerAndSerialNumber(Asn1OpaqueObject issuer, BigInteger certificateSerialNumber) { + this.issuer = issuer; + this.certificateSerialNumber = certificateSerialNumber; + } +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/Pkcs7Constants.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/Pkcs7Constants.java new file mode 100644 index 0000000000..1a115d5156 --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/Pkcs7Constants.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.pkcs7; + +/** + * Assorted PKCS #7 constants from RFC 5652. + */ +public abstract class Pkcs7Constants { + private Pkcs7Constants() {} + + public static final String OID_DATA = "1.2.840.113549.1.7.1"; + public static final String OID_SIGNED_DATA = "1.2.840.113549.1.7.2"; + public static final String OID_CONTENT_TYPE = "1.2.840.113549.1.9.3"; + public static final String OID_MESSAGE_DIGEST = "1.2.840.113549.1.9.4"; +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/Pkcs7DecodingException.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/Pkcs7DecodingException.java new file mode 100644 index 0000000000..4004ee7f78 --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/Pkcs7DecodingException.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.pkcs7; + +/** + * Indicates that an error was encountered while decoding a PKCS #7 structure. + */ +public class Pkcs7DecodingException extends Exception { + private static final long serialVersionUID = 1L; + + public Pkcs7DecodingException(String message) { + super(message); + } + + public Pkcs7DecodingException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/SignedData.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/SignedData.java new file mode 100644 index 0000000000..56b6e502dc --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/SignedData.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.pkcs7; + +import com.android.apksig.internal.asn1.Asn1Class; +import com.android.apksig.internal.asn1.Asn1Field; +import com.android.apksig.internal.asn1.Asn1OpaqueObject; +import com.android.apksig.internal.asn1.Asn1Type; +import com.android.apksig.internal.asn1.Asn1Tagging; +import java.nio.ByteBuffer; +import java.util.List; + +/** + * PKCS #7 {@code SignedData} as specified in RFC 5652. + */ +@Asn1Class(type = Asn1Type.SEQUENCE) +public class SignedData { + + @Asn1Field(index = 0, type = Asn1Type.INTEGER) + public int version; + + @Asn1Field(index = 1, type = Asn1Type.SET_OF) + public List<AlgorithmIdentifier> digestAlgorithms; + + @Asn1Field(index = 2, type = Asn1Type.SEQUENCE) + public EncapsulatedContentInfo encapContentInfo; + + @Asn1Field( + index = 3, + type = Asn1Type.SET_OF, + tagging = Asn1Tagging.IMPLICIT, tagNumber = 0, + optional = true) + public List<Asn1OpaqueObject> certificates; + + @Asn1Field( + index = 4, + type = Asn1Type.SET_OF, + tagging = Asn1Tagging.IMPLICIT, tagNumber = 1, + optional = true) + public List<ByteBuffer> crls; + + @Asn1Field(index = 5, type = Asn1Type.SET_OF) + public List<SignerInfo> signerInfos; +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/SignerIdentifier.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/SignerIdentifier.java new file mode 100644 index 0000000000..a3d70f16bb --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/SignerIdentifier.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.pkcs7; + +import com.android.apksig.internal.asn1.Asn1Class; +import com.android.apksig.internal.asn1.Asn1Field; +import com.android.apksig.internal.asn1.Asn1Type; +import com.android.apksig.internal.asn1.Asn1Tagging; +import java.nio.ByteBuffer; + +/** + * PKCS #7 {@code SignerIdentifier} as specified in RFC 5652. + */ +@Asn1Class(type = Asn1Type.CHOICE) +public class SignerIdentifier { + + @Asn1Field(type = Asn1Type.SEQUENCE) + public IssuerAndSerialNumber issuerAndSerialNumber; + + @Asn1Field(type = Asn1Type.OCTET_STRING, tagging = Asn1Tagging.IMPLICIT, tagNumber = 0) + public ByteBuffer subjectKeyIdentifier; + + public SignerIdentifier() {} + + public SignerIdentifier(IssuerAndSerialNumber issuerAndSerialNumber) { + this.issuerAndSerialNumber = issuerAndSerialNumber; + } +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/SignerInfo.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/SignerInfo.java new file mode 100644 index 0000000000..b885eb8002 --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/SignerInfo.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.pkcs7; + +import com.android.apksig.internal.asn1.Asn1Class; +import com.android.apksig.internal.asn1.Asn1Field; +import com.android.apksig.internal.asn1.Asn1OpaqueObject; +import com.android.apksig.internal.asn1.Asn1Type; +import com.android.apksig.internal.asn1.Asn1Tagging; +import java.nio.ByteBuffer; +import java.util.List; + +/** + * PKCS #7 {@code SignerInfo} as specified in RFC 5652. + */ +@Asn1Class(type = Asn1Type.SEQUENCE) +public class SignerInfo { + + @Asn1Field(index = 0, type = Asn1Type.INTEGER) + public int version; + + @Asn1Field(index = 1, type = Asn1Type.CHOICE) + public SignerIdentifier sid; + + @Asn1Field(index = 2, type = Asn1Type.SEQUENCE) + public AlgorithmIdentifier digestAlgorithm; + + @Asn1Field( + index = 3, + type = Asn1Type.SET_OF, + tagging = Asn1Tagging.IMPLICIT, tagNumber = 0, + optional = true) + public Asn1OpaqueObject signedAttrs; + + @Asn1Field(index = 4, type = Asn1Type.SEQUENCE) + public AlgorithmIdentifier signatureAlgorithm; + + @Asn1Field(index = 5, type = Asn1Type.OCTET_STRING) + public ByteBuffer signature; + + @Asn1Field( + index = 6, + type = Asn1Type.SET_OF, + tagging = Asn1Tagging.IMPLICIT, tagNumber = 1, + optional = true) + public List<Attribute> unsignedAttrs; +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/AndroidSdkVersion.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/AndroidSdkVersion.java new file mode 100644 index 0000000000..90aee30321 --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/AndroidSdkVersion.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.util; + +/** + * Android SDK version / API Level constants. + */ +public abstract class AndroidSdkVersion { + + /** Hidden constructor to prevent instantiation. */ + private AndroidSdkVersion() {} + + /** Android 1.0 */ + public static final int INITIAL_RELEASE = 1; + + /** Android 2.3. */ + public static final int GINGERBREAD = 9; + + /** Android 3.0 */ + public static final int HONEYCOMB = 11; + + /** Android 4.3. The revenge of the beans. */ + public static final int JELLY_BEAN_MR2 = 18; + + /** Android 4.4. KitKat, another tasty treat. */ + public static final int KITKAT = 19; + + /** Android 5.0. A flat one with beautiful shadows. But still tasty. */ + public static final int LOLLIPOP = 21; + + /** Android 6.0. M is for Marshmallow! */ + public static final int M = 23; + + /** Android 7.0. N is for Nougat. */ + public static final int N = 24; + + /** Android O. */ + public static final int O = 26; + + /** Android P. */ + public static final int P = 28; + + /** Android Q. */ + public static final int Q = 29; + + /** Android R. */ + public static final int R = 30; + + /** Android S. */ + public static final int S = 31; + + /** Android Sv2. */ + public static final int Sv2 = 32; + + /** Android Tiramisu. */ + public static final int T = 33; + + /** Android Upside Down Cake. */ + public static final int U = 34; +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/ByteArrayDataSink.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/ByteArrayDataSink.java new file mode 100644 index 0000000000..e5741a5b53 --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/ByteArrayDataSink.java @@ -0,0 +1,240 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.util; + +import com.android.apksig.util.DataSink; +import com.android.apksig.util.DataSource; +import com.android.apksig.util.ReadableDataSink; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Arrays; + +/** + * Growable byte array which can be appended to via {@link DataSink} interface and read from via + * {@link DataSource} interface. + */ +public class ByteArrayDataSink implements ReadableDataSink { + + private static final int MAX_READ_CHUNK_SIZE = 65536; + + private byte[] mArray; + private int mSize; + + public ByteArrayDataSink() { + this(65536); + } + + public ByteArrayDataSink(int initialCapacity) { + if (initialCapacity < 0) { + throw new IllegalArgumentException("initial capacity: " + initialCapacity); + } + mArray = new byte[initialCapacity]; + } + + @Override + public void consume(byte[] buf, int offset, int length) throws IOException { + if (offset < 0) { + // Must perform this check because System.arraycopy below doesn't perform it when + // length == 0 + throw new IndexOutOfBoundsException("offset: " + offset); + } + if (offset > buf.length) { + // Must perform this check because System.arraycopy below doesn't perform it when + // length == 0 + throw new IndexOutOfBoundsException( + "offset: " + offset + ", buf.length: " + buf.length); + } + if (length == 0) { + return; + } + + ensureAvailable(length); + System.arraycopy(buf, offset, mArray, mSize, length); + mSize += length; + } + + @Override + public void consume(ByteBuffer buf) throws IOException { + if (!buf.hasRemaining()) { + return; + } + + if (buf.hasArray()) { + consume(buf.array(), buf.arrayOffset() + buf.position(), buf.remaining()); + buf.position(buf.limit()); + return; + } + + ensureAvailable(buf.remaining()); + byte[] tmp = new byte[Math.min(buf.remaining(), MAX_READ_CHUNK_SIZE)]; + while (buf.hasRemaining()) { + int chunkSize = Math.min(buf.remaining(), tmp.length); + buf.get(tmp, 0, chunkSize); + System.arraycopy(tmp, 0, mArray, mSize, chunkSize); + mSize += chunkSize; + } + } + + private void ensureAvailable(int minAvailable) throws IOException { + if (minAvailable <= 0) { + return; + } + + long minCapacity = ((long) mSize) + minAvailable; + if (minCapacity <= mArray.length) { + return; + } + if (minCapacity > Integer.MAX_VALUE) { + throw new IOException( + "Required capacity too large: " + minCapacity + ", max: " + Integer.MAX_VALUE); + } + int doubleCurrentSize = (int) Math.min(mArray.length * 2L, Integer.MAX_VALUE); + int newSize = (int) Math.max(minCapacity, doubleCurrentSize); + mArray = Arrays.copyOf(mArray, newSize); + } + + @Override + public long size() { + return mSize; + } + + @Override + public ByteBuffer getByteBuffer(long offset, int size) { + checkChunkValid(offset, size); + + // checkChunkValid ensures that it's OK to cast offset to int. + return ByteBuffer.wrap(mArray, (int) offset, size).slice(); + } + + @Override + public void feed(long offset, long size, DataSink sink) throws IOException { + checkChunkValid(offset, size); + + // checkChunkValid ensures that it's OK to cast offset and size to int. + sink.consume(mArray, (int) offset, (int) size); + } + + @Override + public void copyTo(long offset, int size, ByteBuffer dest) throws IOException { + checkChunkValid(offset, size); + + // checkChunkValid ensures that it's OK to cast offset to int. + dest.put(mArray, (int) offset, size); + } + + private void checkChunkValid(long offset, long size) { + if (offset < 0) { + throw new IndexOutOfBoundsException("offset: " + offset); + } + if (size < 0) { + throw new IndexOutOfBoundsException("size: " + size); + } + if (offset > mSize) { + throw new IndexOutOfBoundsException( + "offset (" + offset + ") > source size (" + mSize + ")"); + } + long endOffset = offset + size; + if (endOffset < offset) { + throw new IndexOutOfBoundsException( + "offset (" + offset + ") + size (" + size + ") overflow"); + } + if (endOffset > mSize) { + throw new IndexOutOfBoundsException( + "offset (" + offset + ") + size (" + size + ") > source size (" + mSize + ")"); + } + } + + @Override + public DataSource slice(long offset, long size) { + checkChunkValid(offset, size); + // checkChunkValid ensures that it's OK to cast offset and size to int. + return new SliceDataSource((int) offset, (int) size); + } + + /** + * Slice of the growable byte array. The slice's offset and size in the array are fixed. + */ + private class SliceDataSource implements DataSource { + private final int mSliceOffset; + private final int mSliceSize; + + private SliceDataSource(int offset, int size) { + mSliceOffset = offset; + mSliceSize = size; + } + + @Override + public long size() { + return mSliceSize; + } + + @Override + public void feed(long offset, long size, DataSink sink) throws IOException { + checkChunkValid(offset, size); + // checkChunkValid combined with the way instances of this class are constructed ensures + // that mSliceOffset + offset does not overflow and that it's fine to cast size to int. + sink.consume(mArray, (int) (mSliceOffset + offset), (int) size); + } + + @Override + public ByteBuffer getByteBuffer(long offset, int size) throws IOException { + checkChunkValid(offset, size); + // checkChunkValid combined with the way instances of this class are constructed ensures + // that mSliceOffset + offset does not overflow. + return ByteBuffer.wrap(mArray, (int) (mSliceOffset + offset), size).slice(); + } + + @Override + public void copyTo(long offset, int size, ByteBuffer dest) throws IOException { + checkChunkValid(offset, size); + // checkChunkValid combined with the way instances of this class are constructed ensures + // that mSliceOffset + offset does not overflow. + dest.put(mArray, (int) (mSliceOffset + offset), size); + } + + @Override + public DataSource slice(long offset, long size) { + checkChunkValid(offset, size); + // checkChunkValid combined with the way instances of this class are constructed ensures + // that mSliceOffset + offset does not overflow and that it's fine to cast size to int. + return new SliceDataSource((int) (mSliceOffset + offset), (int) size); + } + + private void checkChunkValid(long offset, long size) { + if (offset < 0) { + throw new IndexOutOfBoundsException("offset: " + offset); + } + if (size < 0) { + throw new IndexOutOfBoundsException("size: " + size); + } + if (offset > mSliceSize) { + throw new IndexOutOfBoundsException( + "offset (" + offset + ") > source size (" + mSliceSize + ")"); + } + long endOffset = offset + size; + if (endOffset < offset) { + throw new IndexOutOfBoundsException( + "offset (" + offset + ") + size (" + size + ") overflow"); + } + if (endOffset > mSliceSize) { + throw new IndexOutOfBoundsException( + "offset (" + offset + ") + size (" + size + ") > source size (" + mSliceSize + + ")"); + } + } + } +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/ByteBufferDataSource.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/ByteBufferDataSource.java new file mode 100644 index 0000000000..656c20e111 --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/ByteBufferDataSource.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.util; + +import com.android.apksig.util.DataSink; +import com.android.apksig.util.DataSource; +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * {@link DataSource} backed by a {@link ByteBuffer}. + */ +public class ByteBufferDataSource implements DataSource { + + private final ByteBuffer mBuffer; + private final int mSize; + + /** + * Constructs a new {@code ByteBufferDigestSource} based on the data contained in the provided + * buffer between the buffer's position and limit. + */ + public ByteBufferDataSource(ByteBuffer buffer) { + this(buffer, true); + } + + /** + * Constructs a new {@code ByteBufferDigestSource} based on the data contained in the provided + * buffer between the buffer's position and limit. + */ + private ByteBufferDataSource(ByteBuffer buffer, boolean sliceRequired) { + mBuffer = (sliceRequired) ? buffer.slice() : buffer; + mSize = buffer.remaining(); + } + + @Override + public long size() { + return mSize; + } + + @Override + public ByteBuffer getByteBuffer(long offset, int size) { + checkChunkValid(offset, size); + + // checkChunkValid ensures that it's OK to cast offset to int. + int chunkPosition = (int) offset; + int chunkLimit = chunkPosition + size; + // Creating a slice of ByteBuffer modifies the state of the source ByteBuffer (position + // and limit fields, to be more specific). We thus use synchronization around these + // state-changing operations to make instances of this class thread-safe. + synchronized (mBuffer) { + // ByteBuffer.limit(int) and .position(int) check that that the position >= limit + // invariant is not broken. Thus, the only way to safely change position and limit + // without caring about their current values is to first set position to 0 or set the + // limit to capacity. + mBuffer.position(0); + + mBuffer.limit(chunkLimit); + mBuffer.position(chunkPosition); + return mBuffer.slice(); + } + } + + @Override + public void copyTo(long offset, int size, ByteBuffer dest) { + dest.put(getByteBuffer(offset, size)); + } + + @Override + public void feed(long offset, long size, DataSink sink) throws IOException { + if ((size < 0) || (size > mSize)) { + throw new IndexOutOfBoundsException("size: " + size + ", source size: " + mSize); + } + sink.consume(getByteBuffer(offset, (int) size)); + } + + @Override + public ByteBufferDataSource slice(long offset, long size) { + if ((offset == 0) && (size == mSize)) { + return this; + } + if ((size < 0) || (size > mSize)) { + throw new IndexOutOfBoundsException("size: " + size + ", source size: " + mSize); + } + return new ByteBufferDataSource( + getByteBuffer(offset, (int) size), + false // no need to slice -- it's already a slice + ); + } + + private void checkChunkValid(long offset, long size) { + if (offset < 0) { + throw new IndexOutOfBoundsException("offset: " + offset); + } + if (size < 0) { + throw new IndexOutOfBoundsException("size: " + size); + } + if (offset > mSize) { + throw new IndexOutOfBoundsException( + "offset (" + offset + ") > source size (" + mSize + ")"); + } + long endOffset = offset + size; + if (endOffset < offset) { + throw new IndexOutOfBoundsException( + "offset (" + offset + ") + size (" + size + ") overflow"); + } + if (endOffset > mSize) { + throw new IndexOutOfBoundsException( + "offset (" + offset + ") + size (" + size + ") > source size (" + mSize +")"); + } + } +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/ByteBufferSink.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/ByteBufferSink.java new file mode 100644 index 0000000000..d7cbe03511 --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/ByteBufferSink.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.util; + +import com.android.apksig.util.DataSink; +import java.io.IOException; +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; + +/** + * Data sink which stores all received data into the associated {@link ByteBuffer}. + */ +public class ByteBufferSink implements DataSink { + + private final ByteBuffer mBuffer; + + public ByteBufferSink(ByteBuffer buffer) { + mBuffer = buffer; + } + + public ByteBuffer getBuffer() { + return mBuffer; + } + + @Override + public void consume(byte[] buf, int offset, int length) throws IOException { + try { + mBuffer.put(buf, offset, length); + } catch (BufferOverflowException e) { + throw new IOException( + "Insufficient space in output buffer for " + length + " bytes", e); + } + } + + @Override + public void consume(ByteBuffer buf) throws IOException { + int length = buf.remaining(); + try { + mBuffer.put(buf); + } catch (BufferOverflowException e) { + throw new IOException( + "Insufficient space in output buffer for " + length + " bytes", e); + } + } +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/ByteBufferUtils.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/ByteBufferUtils.java new file mode 100644 index 0000000000..a7b4b5c804 --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/ByteBufferUtils.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.util; + +import java.nio.ByteBuffer; + +public final class ByteBufferUtils { + private ByteBufferUtils() {} + + /** + * Returns the remaining data of the provided buffer as a new byte array and advances the + * position of the buffer to the buffer's limit. + */ + public static byte[] toByteArray(ByteBuffer buf) { + byte[] result = new byte[buf.remaining()]; + buf.get(result); + return result; + } +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/ByteStreams.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/ByteStreams.java new file mode 100644 index 0000000000..bca3b0827b --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/ByteStreams.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.util; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * Utilities for byte arrays and I/O streams. + */ +public final class ByteStreams { + private ByteStreams() {} + + /** + * Returns the data remaining in the provided input stream as a byte array + */ + public static byte[] toByteArray(InputStream in) throws IOException { + ByteArrayOutputStream result = new ByteArrayOutputStream(); + byte[] buf = new byte[16384]; + int chunkSize; + while ((chunkSize = in.read(buf)) != -1) { + result.write(buf, 0, chunkSize); + } + return result.toByteArray(); + } +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/ChainedDataSource.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/ChainedDataSource.java new file mode 100644 index 0000000000..a0baf1aeff --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/ChainedDataSource.java @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.util; + +import com.android.apksig.util.DataSink; +import com.android.apksig.util.DataSource; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; + +/** Pseudo {@link DataSource} that chains the given {@link DataSource} as a continuous one. */ +public class ChainedDataSource implements DataSource { + + private final DataSource[] mSources; + private final long mTotalSize; + + public ChainedDataSource(DataSource... sources) { + mSources = sources; + mTotalSize = Arrays.stream(sources).mapToLong(src -> src.size()).sum(); + } + + @Override + public long size() { + return mTotalSize; + } + + @Override + public void feed(long offset, long size, DataSink sink) throws IOException { + if (offset + size > mTotalSize) { + throw new IndexOutOfBoundsException("Requested more than available"); + } + + for (DataSource src : mSources) { + // Offset is beyond the current source. Skip. + if (offset >= src.size()) { + offset -= src.size(); + continue; + } + + // If the remaining is enough, finish it. + long remaining = src.size() - offset; + if (remaining >= size) { + src.feed(offset, size, sink); + break; + } + + // If the remaining is not enough, consume all. + src.feed(offset, remaining, sink); + size -= remaining; + offset = 0; + } + } + + @Override + public ByteBuffer getByteBuffer(long offset, int size) throws IOException { + if (offset + size > mTotalSize) { + throw new IndexOutOfBoundsException("Requested more than available"); + } + + // Skip to the first DataSource we need. + Pair<Integer, Long> firstSource = locateDataSource(offset); + int i = firstSource.getFirst(); + offset = firstSource.getSecond(); + + // Return the current source's ByteBuffer if it fits. + if (offset + size <= mSources[i].size()) { + return mSources[i].getByteBuffer(offset, size); + } + + // Otherwise, read into a new buffer. + ByteBuffer buffer = ByteBuffer.allocate(size); + for (; i < mSources.length && buffer.hasRemaining(); i++) { + long sizeToCopy = Math.min(mSources[i].size() - offset, buffer.remaining()); + mSources[i].copyTo(offset, Math.toIntExact(sizeToCopy), buffer); + offset = 0; // may not be zero for the first source, but reset after that. + } + buffer.rewind(); + return buffer; + } + + @Override + public void copyTo(long offset, int size, ByteBuffer dest) throws IOException { + feed(offset, size, new ByteBufferSink(dest)); + } + + @Override + public DataSource slice(long offset, long size) { + // Find the first slice. + Pair<Integer, Long> firstSource = locateDataSource(offset); + int beginIndex = firstSource.getFirst(); + long beginLocalOffset = firstSource.getSecond(); + DataSource beginSource = mSources[beginIndex]; + + if (beginLocalOffset + size <= beginSource.size()) { + return beginSource.slice(beginLocalOffset, size); + } + + // Add the first slice to chaining, followed by the middle full slices, then the last. + ArrayList<DataSource> sources = new ArrayList<>(); + sources.add(beginSource.slice( + beginLocalOffset, beginSource.size() - beginLocalOffset)); + + Pair<Integer, Long> lastSource = locateDataSource(offset + size - 1); + int endIndex = lastSource.getFirst(); + long endLocalOffset = lastSource.getSecond(); + + for (int i = beginIndex + 1; i < endIndex; i++) { + sources.add(mSources[i]); + } + + sources.add(mSources[endIndex].slice(0, endLocalOffset + 1)); + return new ChainedDataSource(sources.toArray(new DataSource[0])); + } + + /** + * Find the index of DataSource that offset is at. + * @return Pair of DataSource index and the local offset in the DataSource. + */ + private Pair<Integer, Long> locateDataSource(long offset) { + long localOffset = offset; + for (int i = 0; i < mSources.length; i++) { + if (localOffset < mSources[i].size()) { + return Pair.of(i, localOffset); + } + localOffset -= mSources[i].size(); + } + throw new IndexOutOfBoundsException("Access is out of bound, offset: " + offset + + ", totalSize: " + mTotalSize); + } +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/DelegatingX509Certificate.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/DelegatingX509Certificate.java new file mode 100644 index 0000000000..2a890f6868 --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/DelegatingX509Certificate.java @@ -0,0 +1,219 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.util; + +import java.math.BigInteger; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.Principal; +import java.security.Provider; +import java.security.PublicKey; +import java.security.SignatureException; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateExpiredException; +import java.security.cert.CertificateNotYetValidException; +import java.security.cert.CertificateParsingException; +import java.security.cert.X509Certificate; +import java.util.Collection; +import java.util.Date; +import java.util.List; +import java.util.Set; + +import javax.security.auth.x500.X500Principal; + +/** + * {@link X509Certificate} which delegates all method invocations to the provided delegate + * {@code X509Certificate}. + */ +public class DelegatingX509Certificate extends X509Certificate { + private static final long serialVersionUID = 1L; + + private final X509Certificate mDelegate; + + public DelegatingX509Certificate(X509Certificate delegate) { + this.mDelegate = delegate; + } + + @Override + public Set<String> getCriticalExtensionOIDs() { + return mDelegate.getCriticalExtensionOIDs(); + } + + @Override + public byte[] getExtensionValue(String oid) { + return mDelegate.getExtensionValue(oid); + } + + @Override + public Set<String> getNonCriticalExtensionOIDs() { + return mDelegate.getNonCriticalExtensionOIDs(); + } + + @Override + public boolean hasUnsupportedCriticalExtension() { + return mDelegate.hasUnsupportedCriticalExtension(); + } + + @Override + public void checkValidity() + throws CertificateExpiredException, CertificateNotYetValidException { + mDelegate.checkValidity(); + } + + @Override + public void checkValidity(Date date) + throws CertificateExpiredException, CertificateNotYetValidException { + mDelegate.checkValidity(date); + } + + @Override + public int getVersion() { + return mDelegate.getVersion(); + } + + @Override + public BigInteger getSerialNumber() { + return mDelegate.getSerialNumber(); + } + + @Override + public Principal getIssuerDN() { + return mDelegate.getIssuerDN(); + } + + @Override + public Principal getSubjectDN() { + return mDelegate.getSubjectDN(); + } + + @Override + public Date getNotBefore() { + return mDelegate.getNotBefore(); + } + + @Override + public Date getNotAfter() { + return mDelegate.getNotAfter(); + } + + @Override + public byte[] getTBSCertificate() throws CertificateEncodingException { + return mDelegate.getTBSCertificate(); + } + + @Override + public byte[] getSignature() { + return mDelegate.getSignature(); + } + + @Override + public String getSigAlgName() { + return mDelegate.getSigAlgName(); + } + + @Override + public String getSigAlgOID() { + return mDelegate.getSigAlgOID(); + } + + @Override + public byte[] getSigAlgParams() { + return mDelegate.getSigAlgParams(); + } + + @Override + public boolean[] getIssuerUniqueID() { + return mDelegate.getIssuerUniqueID(); + } + + @Override + public boolean[] getSubjectUniqueID() { + return mDelegate.getSubjectUniqueID(); + } + + @Override + public boolean[] getKeyUsage() { + return mDelegate.getKeyUsage(); + } + + @Override + public int getBasicConstraints() { + return mDelegate.getBasicConstraints(); + } + + @Override + public byte[] getEncoded() throws CertificateEncodingException { + return mDelegate.getEncoded(); + } + + @Override + public void verify(PublicKey key) throws CertificateException, NoSuchAlgorithmException, + InvalidKeyException, NoSuchProviderException, SignatureException { + mDelegate.verify(key); + } + + @Override + public void verify(PublicKey key, String sigProvider) + throws CertificateException, NoSuchAlgorithmException, InvalidKeyException, + NoSuchProviderException, SignatureException { + mDelegate.verify(key, sigProvider); + } + + @Override + public String toString() { + return mDelegate.toString(); + } + + @Override + public PublicKey getPublicKey() { + return mDelegate.getPublicKey(); + } + + @Override + public X500Principal getIssuerX500Principal() { + return mDelegate.getIssuerX500Principal(); + } + + @Override + public X500Principal getSubjectX500Principal() { + return mDelegate.getSubjectX500Principal(); + } + + @Override + public List<String> getExtendedKeyUsage() throws CertificateParsingException { + return mDelegate.getExtendedKeyUsage(); + } + + @Override + public Collection<List<?>> getSubjectAlternativeNames() throws CertificateParsingException { + return mDelegate.getSubjectAlternativeNames(); + } + + @Override + public Collection<List<?>> getIssuerAlternativeNames() throws CertificateParsingException { + return mDelegate.getIssuerAlternativeNames(); + } + + @Override + @SuppressWarnings("AndroidJdkLibsChecker") + public void verify(PublicKey key, Provider sigProvider) throws CertificateException, + NoSuchAlgorithmException, InvalidKeyException, SignatureException { + mDelegate.verify(key, sigProvider); + } +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/FileChannelDataSource.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/FileChannelDataSource.java new file mode 100644 index 0000000000..e4a421a72c --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/FileChannelDataSource.java @@ -0,0 +1,191 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.util; + +import com.android.apksig.util.DataSink; +import com.android.apksig.util.DataSource; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; + +/** + * {@link DataSource} backed by a {@link FileChannel} for {@link RandomAccessFile} access. + */ +public class FileChannelDataSource implements DataSource { + + private static final int MAX_READ_CHUNK_SIZE = 1024 * 1024; + + private final FileChannel mChannel; + private final long mOffset; + private final long mSize; + + /** + * Constructs a new {@code FileChannelDataSource} based on the data contained in the + * whole file. Changes to the contents of the file, including the size of the file, + * will be visible in this data source. + */ + public FileChannelDataSource(FileChannel channel) { + mChannel = channel; + mOffset = 0; + mSize = -1; + } + + /** + * Constructs a new {@code FileChannelDataSource} based on the data contained in the + * specified region of the provided file. Changes to the contents of the file will be visible in + * this data source. + * + * @throws IndexOutOfBoundsException if {@code offset} or {@code size} is negative. + */ + public FileChannelDataSource(FileChannel channel, long offset, long size) { + if (offset < 0) { + throw new IndexOutOfBoundsException("offset: " + size); + } + if (size < 0) { + throw new IndexOutOfBoundsException("size: " + size); + } + mChannel = channel; + mOffset = offset; + mSize = size; + } + + @Override + public long size() { + if (mSize == -1) { + try { + return mChannel.size(); + } catch (IOException e) { + return 0; + } + } else { + return mSize; + } + } + + @Override + public FileChannelDataSource slice(long offset, long size) { + long sourceSize = size(); + checkChunkValid(offset, size, sourceSize); + if ((offset == 0) && (size == sourceSize)) { + return this; + } + + return new FileChannelDataSource(mChannel, mOffset + offset, size); + } + + @Override + public void feed(long offset, long size, DataSink sink) throws IOException { + long sourceSize = size(); + checkChunkValid(offset, size, sourceSize); + if (size == 0) { + return; + } + + long chunkOffsetInFile = mOffset + offset; + long remaining = size; + ByteBuffer buf = ByteBuffer.allocateDirect((int) Math.min(remaining, MAX_READ_CHUNK_SIZE)); + + while (remaining > 0) { + int chunkSize = (int) Math.min(remaining, buf.capacity()); + int chunkRemaining = chunkSize; + buf.limit(chunkSize); + synchronized (mChannel) { + mChannel.position(chunkOffsetInFile); + while (chunkRemaining > 0) { + int read = mChannel.read(buf); + if (read < 0) { + throw new IOException("Unexpected EOF encountered"); + } + chunkRemaining -= read; + } + } + buf.flip(); + sink.consume(buf); + buf.clear(); + chunkOffsetInFile += chunkSize; + remaining -= chunkSize; + } + } + + @Override + public void copyTo(long offset, int size, ByteBuffer dest) throws IOException { + long sourceSize = size(); + checkChunkValid(offset, size, sourceSize); + if (size == 0) { + return; + } + if (size > dest.remaining()) { + throw new BufferOverflowException(); + } + + long offsetInFile = mOffset + offset; + int remaining = size; + int prevLimit = dest.limit(); + try { + // FileChannel.read(ByteBuffer) reads up to dest.remaining(). Thus, we need to adjust + // the buffer's limit to avoid reading more than size bytes. + dest.limit(dest.position() + size); + while (remaining > 0) { + int chunkSize; + synchronized (mChannel) { + mChannel.position(offsetInFile); + chunkSize = mChannel.read(dest); + } + offsetInFile += chunkSize; + remaining -= chunkSize; + } + } finally { + dest.limit(prevLimit); + } + } + + @Override + public ByteBuffer getByteBuffer(long offset, int size) throws IOException { + if (size < 0) { + throw new IndexOutOfBoundsException("size: " + size); + } + ByteBuffer result = ByteBuffer.allocate(size); + copyTo(offset, size, result); + result.flip(); + return result; + } + + private static void checkChunkValid(long offset, long size, long sourceSize) { + if (offset < 0) { + throw new IndexOutOfBoundsException("offset: " + offset); + } + if (size < 0) { + throw new IndexOutOfBoundsException("size: " + size); + } + if (offset > sourceSize) { + throw new IndexOutOfBoundsException( + "offset (" + offset + ") > source size (" + sourceSize + ")"); + } + long endOffset = offset + size; + if (endOffset < offset) { + throw new IndexOutOfBoundsException( + "offset (" + offset + ") + size (" + size + ") overflow"); + } + if (endOffset > sourceSize) { + throw new IndexOutOfBoundsException( + "offset (" + offset + ") + size (" + size + + ") > source size (" + sourceSize +")"); + } + } +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/GuaranteedEncodedFormX509Certificate.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/GuaranteedEncodedFormX509Certificate.java new file mode 100644 index 0000000000..958cd12aa3 --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/GuaranteedEncodedFormX509Certificate.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.util; + +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; +import java.util.Arrays; + +/** + * {@link X509Certificate} whose {@link #getEncoded()} returns the data provided at construction + * time. + */ +public class GuaranteedEncodedFormX509Certificate extends DelegatingX509Certificate { + private static final long serialVersionUID = 1L; + + private final byte[] mEncodedForm; + private int mHash = -1; + + public GuaranteedEncodedFormX509Certificate(X509Certificate wrapped, byte[] encodedForm) { + super(wrapped); + this.mEncodedForm = (encodedForm != null) ? encodedForm.clone() : null; + } + + @Override + public byte[] getEncoded() throws CertificateEncodingException { + return (mEncodedForm != null) ? mEncodedForm.clone() : null; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof X509Certificate)) return false; + + try { + byte[] a = this.getEncoded(); + byte[] b = ((X509Certificate) o).getEncoded(); + return Arrays.equals(a, b); + } catch (CertificateEncodingException e) { + return false; + } + } + + @Override + public int hashCode() { + if (mHash == -1) { + try { + mHash = Arrays.hashCode(this.getEncoded()); + } catch (CertificateEncodingException e) { + mHash = 0; + } + } + return mHash; + } +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/InclusiveIntRange.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/InclusiveIntRange.java new file mode 100644 index 0000000000..d7866a9edd --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/InclusiveIntRange.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.util; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Inclusive interval of integers. + */ +public class InclusiveIntRange { + private final int min; + private final int max; + + private InclusiveIntRange(int min, int max) { + this.min = min; + this.max = max; + } + + public int getMin() { + return min; + } + + public int getMax() { + return max; + } + + public static InclusiveIntRange fromTo(int min, int max) { + return new InclusiveIntRange(min, max); + } + + public static InclusiveIntRange from(int min) { + return new InclusiveIntRange(min, Integer.MAX_VALUE); + } + + public List<InclusiveIntRange> getValuesNotIn( + List<InclusiveIntRange> sortedNonOverlappingRanges) { + if (sortedNonOverlappingRanges.isEmpty()) { + return Collections.singletonList(this); + } + + int testValue = min; + List<InclusiveIntRange> result = null; + for (InclusiveIntRange range : sortedNonOverlappingRanges) { + int rangeMax = range.max; + if (testValue > rangeMax) { + continue; + } + int rangeMin = range.min; + if (testValue < range.min) { + if (result == null) { + result = new ArrayList<>(); + } + result.add(fromTo(testValue, rangeMin - 1)); + } + if (rangeMax >= max) { + return (result != null) ? result : Collections.emptyList(); + } + testValue = rangeMax + 1; + } + if (testValue <= max) { + if (result == null) { + result = new ArrayList<>(1); + } + result.add(fromTo(testValue, max)); + } + return (result != null) ? result : Collections.emptyList(); + } + + @Override + public String toString() { + return "[" + min + ", " + ((max < Integer.MAX_VALUE) ? (max + "]") : "\u221e)"); + } +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/MessageDigestSink.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/MessageDigestSink.java new file mode 100644 index 0000000000..733dd563ce --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/MessageDigestSink.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.apksig.internal.util; + +import com.android.apksig.util.DataSink; +import java.nio.ByteBuffer; +import java.security.MessageDigest; + +/** + * Data sink which feeds all received data into the associated {@link MessageDigest} instances. Each + * {@code MessageDigest} instance receives the same data. + */ +public class MessageDigestSink implements DataSink { + + private final MessageDigest[] mMessageDigests; + + public MessageDigestSink(MessageDigest[] digests) { + mMessageDigests = digests; + } + + @Override + public void consume(byte[] buf, int offset, int length) { + for (MessageDigest md : mMessageDigests) { + md.update(buf, offset, length); + } + } + + @Override + public void consume(ByteBuffer buf) { + int originalPosition = buf.position(); + for (MessageDigest md : mMessageDigests) { + // Reset the position back to the original because the previous iteration's + // MessageDigest.update set the buffer's position to the buffer's limit. + buf.position(originalPosition); + md.update(buf); + } + } +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/OutputStreamDataSink.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/OutputStreamDataSink.java new file mode 100644 index 0000000000..f1b5ac6c5e --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/OutputStreamDataSink.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.util; + +import com.android.apksig.util.DataSink; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.ByteBuffer; + +/** + * {@link DataSink} which outputs received data into the associated {@link OutputStream}. + */ +public class OutputStreamDataSink implements DataSink { + + private static final int MAX_READ_CHUNK_SIZE = 65536; + + private final OutputStream mOut; + + /** + * Constructs a new {@code OutputStreamDataSink} which outputs received data into the provided + * {@link OutputStream}. + */ + public OutputStreamDataSink(OutputStream out) { + if (out == null) { + throw new NullPointerException("out == null"); + } + mOut = out; + } + + /** + * Returns {@link OutputStream} into which this data sink outputs received data. + */ + public OutputStream getOutputStream() { + return mOut; + } + + @Override + public void consume(byte[] buf, int offset, int length) throws IOException { + mOut.write(buf, offset, length); + } + + @Override + public void consume(ByteBuffer buf) throws IOException { + if (!buf.hasRemaining()) { + return; + } + + if (buf.hasArray()) { + mOut.write( + buf.array(), + buf.arrayOffset() + buf.position(), + buf.remaining()); + buf.position(buf.limit()); + } else { + byte[] tmp = new byte[Math.min(buf.remaining(), MAX_READ_CHUNK_SIZE)]; + while (buf.hasRemaining()) { + int chunkSize = Math.min(buf.remaining(), tmp.length); + buf.get(tmp, 0, chunkSize); + mOut.write(tmp, 0, chunkSize); + } + } + } +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/Pair.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/Pair.java new file mode 100644 index 0000000000..7f9ee520f1 --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/Pair.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.util; + +/** + * Pair of two elements. + */ +public final class Pair<A, B> { + private final A mFirst; + private final B mSecond; + + private Pair(A first, B second) { + mFirst = first; + mSecond = second; + } + + public static <A, B> Pair<A, B> of(A first, B second) { + return new Pair<A, B>(first, second); + } + + public A getFirst() { + return mFirst; + } + + public B getSecond() { + return mSecond; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((mFirst == null) ? 0 : mFirst.hashCode()); + result = prime * result + ((mSecond == null) ? 0 : mSecond.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + @SuppressWarnings("rawtypes") + Pair other = (Pair) obj; + if (mFirst == null) { + if (other.mFirst != null) { + return false; + } + } else if (!mFirst.equals(other.mFirst)) { + return false; + } + if (mSecond == null) { + if (other.mSecond != null) { + return false; + } + } else if (!mSecond.equals(other.mSecond)) { + return false; + } + return true; + } +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/RandomAccessFileDataSink.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/RandomAccessFileDataSink.java new file mode 100644 index 0000000000..bbd2d14a8a --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/RandomAccessFileDataSink.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.util; + +import com.android.apksig.util.DataSink; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; + +/** + * {@link DataSink} which outputs received data into the associated file, sequentially. + */ +public class RandomAccessFileDataSink implements DataSink { + + private final RandomAccessFile mFile; + private final FileChannel mFileChannel; + private long mPosition; + + /** + * Constructs a new {@code RandomAccessFileDataSink} which stores output starting from the + * beginning of the provided file. + */ + public RandomAccessFileDataSink(RandomAccessFile file) { + this(file, 0); + } + + /** + * Constructs a new {@code RandomAccessFileDataSink} which stores output starting from the + * specified position of the provided file. + */ + public RandomAccessFileDataSink(RandomAccessFile file, long startPosition) { + if (file == null) { + throw new NullPointerException("file == null"); + } + if (startPosition < 0) { + throw new IllegalArgumentException("startPosition: " + startPosition); + } + mFile = file; + mFileChannel = file.getChannel(); + mPosition = startPosition; + } + + /** + * Returns the underlying {@link RandomAccessFile}. + */ + public RandomAccessFile getFile() { + return mFile; + } + + @Override + public void consume(byte[] buf, int offset, int length) throws IOException { + if (offset < 0) { + // Must perform this check here because RandomAccessFile.write doesn't throw when offset + // is negative but length is 0 + throw new IndexOutOfBoundsException("offset: " + offset); + } + if (offset > buf.length) { + // Must perform this check here because RandomAccessFile.write doesn't throw when offset + // is too large but length is 0 + throw new IndexOutOfBoundsException( + "offset: " + offset + ", buf.length: " + buf.length); + } + if (length == 0) { + return; + } + + synchronized (mFile) { + mFile.seek(mPosition); + mFile.write(buf, offset, length); + mPosition += length; + } + } + + @Override + public void consume(ByteBuffer buf) throws IOException { + int length = buf.remaining(); + if (length == 0) { + return; + } + + synchronized (mFile) { + mFile.seek(mPosition); + while (buf.hasRemaining()) { + mFileChannel.write(buf); + } + mPosition += length; + } + } +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/TeeDataSink.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/TeeDataSink.java new file mode 100644 index 0000000000..2e46f18b05 --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/TeeDataSink.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.util; + +import com.android.apksig.util.DataSink; +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * {@link DataSink} which copies provided input into each of the sinks provided to it. + */ +public class TeeDataSink implements DataSink { + + private final DataSink[] mSinks; + + public TeeDataSink(DataSink[] sinks) { + mSinks = sinks; + } + + @Override + public void consume(byte[] buf, int offset, int length) throws IOException { + for (DataSink sink : mSinks) { + sink.consume(buf, offset, length); + } + } + + @Override + public void consume(ByteBuffer buf) throws IOException { + int originalPosition = buf.position(); + for (int i = 0; i < mSinks.length; i++) { + if (i > 0) { + buf.position(originalPosition); + } + mSinks[i].consume(buf); + } + } +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/VerityTreeBuilder.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/VerityTreeBuilder.java new file mode 100644 index 0000000000..81026ba5ff --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/VerityTreeBuilder.java @@ -0,0 +1,325 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.util; + +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +import com.android.apksig.internal.zip.ZipUtils; +import com.android.apksig.util.DataSink; +import com.android.apksig.util.DataSource; +import com.android.apksig.util.DataSources; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +import java.util.ArrayList; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Phaser; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +/** + * VerityTreeBuilder is used to generate the root hash of verity tree built from the input file. + * The root hash can be used on device for on-access verification. The tree itself is reproducible + * on device, and is not shipped with the APK. + */ +public class VerityTreeBuilder implements AutoCloseable { + + /** + * Maximum size (in bytes) of each node of the tree. + */ + private final static int CHUNK_SIZE = 4096; + /** + * Maximum parallelism while calculating digests. + */ + private final static int DIGEST_PARALLELISM = Math.min(32, + Runtime.getRuntime().availableProcessors()); + /** + * Queue size. + */ + private final static int MAX_OUTSTANDING_CHUNKS = 4; + /** + * Typical prefetch size. + */ + private final static int MAX_PREFETCH_CHUNKS = 1024; + /** + * Minimum chunks to be processed by a single worker task. + */ + private final static int MIN_CHUNKS_PER_WORKER = 8; + + /** + * Digest algorithm (JCA Digest algorithm name) used in the tree. + */ + private final static String JCA_ALGORITHM = "SHA-256"; + + /** + * Optional salt to apply before each digestion. + */ + private final byte[] mSalt; + + private final MessageDigest mMd; + + private final ExecutorService mExecutor = + new ThreadPoolExecutor(DIGEST_PARALLELISM, DIGEST_PARALLELISM, + 0L, MILLISECONDS, + new ArrayBlockingQueue<>(MAX_OUTSTANDING_CHUNKS), + new ThreadPoolExecutor.CallerRunsPolicy()); + + public VerityTreeBuilder(byte[] salt) throws NoSuchAlgorithmException { + mSalt = salt; + mMd = getNewMessageDigest(); + } + + @Override + public void close() { + mExecutor.shutdownNow(); + } + + /** + * Returns the root hash of the APK verity tree built from ZIP blocks. + * + * Specifically, APK verity tree is built from the APK, but as if the APK Signing Block (which + * must be page aligned) and the "Central Directory offset" field in End of Central Directory + * are skipped. + */ + public byte[] generateVerityTreeRootHash(DataSource beforeApkSigningBlock, + DataSource centralDir, DataSource eocd) throws IOException { + if (beforeApkSigningBlock.size() % CHUNK_SIZE != 0) { + throw new IllegalStateException("APK Signing Block size not a multiple of " + CHUNK_SIZE + + ": " + beforeApkSigningBlock.size()); + } + + // Ensure that, when digesting, ZIP End of Central Directory record's Central Directory + // offset field is treated as pointing to the offset at which the APK Signing Block will + // start. + long centralDirOffsetForDigesting = beforeApkSigningBlock.size(); + ByteBuffer eocdBuf = ByteBuffer.allocate((int) eocd.size()); + eocdBuf.order(ByteOrder.LITTLE_ENDIAN); + eocd.copyTo(0, (int) eocd.size(), eocdBuf); + eocdBuf.flip(); + ZipUtils.setZipEocdCentralDirectoryOffset(eocdBuf, centralDirOffsetForDigesting); + + return generateVerityTreeRootHash(new ChainedDataSource(beforeApkSigningBlock, centralDir, + DataSources.asDataSource(eocdBuf))); + } + + /** + * Returns the root hash of the verity tree built from the data source. + */ + public byte[] generateVerityTreeRootHash(DataSource fileSource) throws IOException { + ByteBuffer verityBuffer = generateVerityTree(fileSource); + return getRootHashFromTree(verityBuffer); + } + + /** + * Returns the byte buffer that contains the whole verity tree. + * + * The tree is built bottom up. The bottom level has 256-bit digest for each 4 KB block in the + * input file. If the total size is larger than 4 KB, take this level as input and repeat the + * same procedure, until the level is within 4 KB. If salt is given, it will apply to each + * digestion before the actual data. + * + * The returned root hash is calculated from the last level of 4 KB chunk, similarly with salt. + * + * The tree is currently stored only in memory and is never written out. Nevertheless, it is + * the actual verity tree format on disk, and is supposed to be re-generated on device. + */ + public ByteBuffer generateVerityTree(DataSource fileSource) throws IOException { + int digestSize = mMd.getDigestLength(); + + // Calculate the summed area table of level size. In other word, this is the offset + // table of each level, plus the next non-existing level. + int[] levelOffset = calculateLevelOffset(fileSource.size(), digestSize); + + ByteBuffer verityBuffer = ByteBuffer.allocate(levelOffset[levelOffset.length - 1]); + + // Generate the hash tree bottom-up. + for (int i = levelOffset.length - 2; i >= 0; i--) { + DataSink middleBufferSink = new ByteBufferSink( + slice(verityBuffer, levelOffset[i], levelOffset[i + 1])); + DataSource src; + if (i == levelOffset.length - 2) { + src = fileSource; + digestDataByChunks(src, middleBufferSink); + } else { + src = DataSources.asDataSource(slice(verityBuffer.asReadOnlyBuffer(), + levelOffset[i + 1], levelOffset[i + 2])); + digestDataByChunks(src, middleBufferSink); + } + + // If the output is not full chunk, pad with 0s. + long totalOutput = divideRoundup(src.size(), CHUNK_SIZE) * digestSize; + int incomplete = (int) (totalOutput % CHUNK_SIZE); + if (incomplete > 0) { + byte[] padding = new byte[CHUNK_SIZE - incomplete]; + middleBufferSink.consume(padding, 0, padding.length); + } + } + return verityBuffer; + } + + /** + * Returns the digested root hash from the top level (only page) of a verity tree. + */ + public byte[] getRootHashFromTree(ByteBuffer verityBuffer) throws IOException { + ByteBuffer firstPage = slice(verityBuffer.asReadOnlyBuffer(), 0, CHUNK_SIZE); + return saltedDigest(firstPage); + } + + /** + * Returns an array of summed area table of level size in the verity tree. In other words, the + * returned array is offset of each level in the verity tree file format, plus an additional + * offset of the next non-existing level (i.e. end of the last level + 1). Thus the array size + * is level + 1. + */ + private static int[] calculateLevelOffset(long dataSize, int digestSize) { + // Compute total size of each level, bottom to top. + ArrayList<Long> levelSize = new ArrayList<>(); + while (true) { + long chunkCount = divideRoundup(dataSize, CHUNK_SIZE); + long size = CHUNK_SIZE * divideRoundup(chunkCount * digestSize, CHUNK_SIZE); + levelSize.add(size); + if (chunkCount * digestSize <= CHUNK_SIZE) { + break; + } + dataSize = chunkCount * digestSize; + } + + // Reverse and convert to summed area table. + int[] levelOffset = new int[levelSize.size() + 1]; + levelOffset[0] = 0; + for (int i = 0; i < levelSize.size(); i++) { + // We don't support verity tree if it is larger then Integer.MAX_VALUE. + levelOffset[i + 1] = levelOffset[i] + Math.toIntExact( + levelSize.get(levelSize.size() - i - 1)); + } + return levelOffset; + } + + /** + * Digest data source by chunks then feeds them to the sink one by one. If the last unit is + * less than the chunk size and padding is desired, feed with extra padding 0 to fill up the + * chunk before digesting. + */ + private void digestDataByChunks(DataSource dataSource, DataSink dataSink) throws IOException { + final long size = dataSource.size(); + final int chunks = (int) divideRoundup(size, CHUNK_SIZE); + + /** Single IO operation size, in chunks. */ + final int ioSizeChunks = MAX_PREFETCH_CHUNKS; + + final byte[][] hashes = new byte[chunks][]; + + Phaser tasks = new Phaser(1); + + // Reading the input file as fast as we can. + final long maxReadSize = ioSizeChunks * CHUNK_SIZE; + + long readOffset = 0; + int startChunkIndex = 0; + while (readOffset < size) { + final long readLimit = Math.min(readOffset + maxReadSize, size); + final int readSize = (int) (readLimit - readOffset); + final int bufferSizeChunks = (int) divideRoundup(readSize, CHUNK_SIZE); + + // Overllocating to zero-pad last chunk. + // With 4MiB block size, 32 threads and 4 queue size we might allocate up to 144MiB. + final ByteBuffer buffer = ByteBuffer.allocate(bufferSizeChunks * CHUNK_SIZE); + dataSource.copyTo(readOffset, readSize, buffer); + buffer.rewind(); + + final int readChunkIndex = startChunkIndex; + Runnable task = () -> { + final MessageDigest md = cloneMessageDigest(); + for (int offset = 0, finish = buffer.capacity(), chunkIndex = readChunkIndex; + offset < finish; offset += CHUNK_SIZE, ++chunkIndex) { + ByteBuffer chunk = slice(buffer, offset, offset + CHUNK_SIZE); + hashes[chunkIndex] = saltedDigest(md, chunk); + } + tasks.arriveAndDeregister(); + }; + tasks.register(); + mExecutor.execute(task); + + startChunkIndex += bufferSizeChunks; + readOffset += readSize; + } + + // Waiting for the tasks to complete. + tasks.arriveAndAwaitAdvance(); + + // Streaming hashes back. + for (byte[] hash : hashes) { + dataSink.consume(hash, 0, hash.length); + } + } + + /** Returns the digest of data with salt prepended. */ + private byte[] saltedDigest(ByteBuffer data) { + return saltedDigest(mMd, data); + } + + private byte[] saltedDigest(MessageDigest md, ByteBuffer data) { + md.reset(); + if (mSalt != null) { + md.update(mSalt); + } + md.update(data); + return md.digest(); + } + + /** Divides a number and round up to the closest integer. */ + private static long divideRoundup(long dividend, long divisor) { + return (dividend + divisor - 1) / divisor; + } + + /** Returns a slice of the buffer with shared the content. */ + private static ByteBuffer slice(ByteBuffer buffer, int begin, int end) { + ByteBuffer b = buffer.duplicate(); + b.position(0); // to ensure position <= limit invariant. + b.limit(end); + b.position(begin); + return b.slice(); + } + + /** + * Obtains a new instance of the message digest algorithm. + */ + private static MessageDigest getNewMessageDigest() throws NoSuchAlgorithmException { + return MessageDigest.getInstance(JCA_ALGORITHM); + } + + /** + * Clones the existing message digest, or creates a new instance if clone is unavailable. + */ + private MessageDigest cloneMessageDigest() { + try { + return (MessageDigest) mMd.clone(); + } catch (CloneNotSupportedException ignored) { + try { + return getNewMessageDigest(); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException( + "Failed to obtain an instance of a previously available message digest", e); + } + } + } +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/X509CertificateUtils.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/X509CertificateUtils.java new file mode 100644 index 0000000000..ca6271df2a --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/util/X509CertificateUtils.java @@ -0,0 +1,282 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.util; + +import com.android.apksig.internal.asn1.Asn1BerParser; +import com.android.apksig.internal.asn1.Asn1DecodingException; +import com.android.apksig.internal.asn1.Asn1DerEncoder; +import com.android.apksig.internal.asn1.Asn1EncodingException; +import com.android.apksig.internal.x509.Certificate; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Base64; +import java.util.Collection; + +/** + * Provides methods to generate {@code X509Certificate}s from their encoded form. These methods + * can be used to generate certificates that would be rejected by the Java {@code + * CertificateFactory}. + */ +public class X509CertificateUtils { + + private static volatile CertificateFactory sCertFactory = null; + + // The PEM certificate header and footer as specified in RFC 7468: + // There is exactly one space character (SP) separating the "BEGIN" or + // "END" from the label. There are exactly five hyphen-minus (also + // known as dash) characters ("-") on both ends of the encapsulation + // boundaries, no more, no less. + public static final byte[] BEGIN_CERT_HEADER = "-----BEGIN CERTIFICATE-----".getBytes(); + public static final byte[] END_CERT_FOOTER = "-----END CERTIFICATE-----".getBytes(); + + private static void buildCertFactory() { + if (sCertFactory != null) { + return; + } + + buildCertFactoryHelper(); + } + + private static synchronized void buildCertFactoryHelper() { + if (sCertFactory != null) { + return; + } + try { + sCertFactory = CertificateFactory.getInstance("X.509"); + } catch (CertificateException e) { + throw new RuntimeException("Failed to create X.509 CertificateFactory", e); + } + } + + /** + * Generates an {@code X509Certificate} from the {@code InputStream}. + * + * @throws CertificateException if the {@code InputStream} cannot be decoded to a valid + * certificate. + */ + public static X509Certificate generateCertificate(InputStream in) throws CertificateException { + byte[] encodedForm; + try { + encodedForm = ByteStreams.toByteArray(in); + } catch (IOException e) { + throw new CertificateException("Failed to parse certificate", e); + } + return generateCertificate(encodedForm); + } + + /** + * Generates an {@code X509Certificate} from the encoded form. + * + * @throws CertificateException if the encodedForm cannot be decoded to a valid certificate. + */ + public static X509Certificate generateCertificate(byte[] encodedForm) + throws CertificateException { + buildCertFactory(); + return generateCertificate(encodedForm, sCertFactory); + } + + /** + * Generates an {@code X509Certificate} from the encoded form using the provided + * {@code CertificateFactory}. + * + * @throws CertificateException if the encodedForm cannot be decoded to a valid certificate. + */ + public static X509Certificate generateCertificate(byte[] encodedForm, + CertificateFactory certFactory) throws CertificateException { + X509Certificate certificate; + try { + certificate = (X509Certificate) certFactory.generateCertificate( + new ByteArrayInputStream(encodedForm)); + return certificate; + } catch (CertificateException e) { + // This could be expected if the certificate is encoded using a BER encoding that does + // not use the minimum number of bytes to represent the length of the contents; attempt + // to decode the certificate using the BER parser and re-encode using the DER encoder + // below. + } + try { + // Some apps were previously signed with a BER encoded certificate that now results + // in exceptions from the CertificateFactory generateCertificate(s) methods. Since + // the original BER encoding of the certificate is used as the signature for these + // apps that original encoding must be maintained when signing updated versions of + // these apps and any new apps that may require capabilities guarded by the + // signature. To maintain the same signature the BER parser can be used to parse + // the certificate, then it can be re-encoded to its DER equivalent which is + // accepted by the generateCertificate method. The positions in the ByteBuffer can + // then be used with the GuaranteedEncodedFormX509Certificate object to ensure the + // getEncoded method returns the original signature of the app. + ByteBuffer encodedCertBuffer = getNextDEREncodedCertificateBlock( + ByteBuffer.wrap(encodedForm)); + int startingPos = encodedCertBuffer.position(); + Certificate reencodedCert = Asn1BerParser.parse(encodedCertBuffer, Certificate.class); + byte[] reencodedForm = Asn1DerEncoder.encode(reencodedCert); + certificate = (X509Certificate) certFactory.generateCertificate( + new ByteArrayInputStream(reencodedForm)); + // If the reencodedForm is successfully accepted by the CertificateFactory then copy the + // original encoding from the ByteBuffer and use that encoding in the Guaranteed object. + byte[] originalEncoding = new byte[encodedCertBuffer.position() - startingPos]; + encodedCertBuffer.position(startingPos); + encodedCertBuffer.get(originalEncoding); + GuaranteedEncodedFormX509Certificate guaranteedEncodedCert = + new GuaranteedEncodedFormX509Certificate(certificate, originalEncoding); + return guaranteedEncodedCert; + } catch (Asn1DecodingException | Asn1EncodingException | CertificateException e) { + throw new CertificateException("Failed to parse certificate", e); + } + } + + /** + * Generates a {@code Collection} of {@code Certificate} objects from the encoded {@code + * InputStream}. + * + * @throws CertificateException if the InputStream cannot be decoded to zero or more valid + * {@code Certificate} objects. + */ + public static Collection<? extends java.security.cert.Certificate> generateCertificates( + InputStream in) throws CertificateException { + buildCertFactory(); + return generateCertificates(in, sCertFactory); + } + + /** + * Generates a {@code Collection} of {@code Certificate} objects from the encoded {@code + * InputStream} using the provided {@code CertificateFactory}. + * + * @throws CertificateException if the InputStream cannot be decoded to zero or more valid + * {@code Certificates} objects. + */ + public static Collection<? extends java.security.cert.Certificate> generateCertificates( + InputStream in, CertificateFactory certFactory) throws CertificateException { + // Since the InputStream is not guaranteed to support mark / reset operations first read it + // into a byte array to allow using the BER parser / DER encoder if it cannot be read by + // the CertificateFactory. + byte[] encodedCerts; + try { + encodedCerts = ByteStreams.toByteArray(in); + } catch (IOException e) { + throw new CertificateException("Failed to read the input stream", e); + } + try { + return certFactory.generateCertificates(new ByteArrayInputStream(encodedCerts)); + } catch (CertificateException e) { + // This could be expected if the certificates are encoded using a BER encoding that does + // not use the minimum number of bytes to represent the length of the contents; attempt + // to decode the certificates using the BER parser and re-encode using the DER encoder + // below. + } + try { + Collection<X509Certificate> certificates = new ArrayList<>(1); + ByteBuffer encodedCertsBuffer = ByteBuffer.wrap(encodedCerts); + while (encodedCertsBuffer.hasRemaining()) { + ByteBuffer certBuffer = getNextDEREncodedCertificateBlock(encodedCertsBuffer); + int startingPos = certBuffer.position(); + Certificate reencodedCert = Asn1BerParser.parse(certBuffer, Certificate.class); + byte[] reencodedForm = Asn1DerEncoder.encode(reencodedCert); + X509Certificate certificate = (X509Certificate) certFactory.generateCertificate( + new ByteArrayInputStream(reencodedForm)); + byte[] originalEncoding = new byte[certBuffer.position() - startingPos]; + certBuffer.position(startingPos); + certBuffer.get(originalEncoding); + GuaranteedEncodedFormX509Certificate guaranteedEncodedCert = + new GuaranteedEncodedFormX509Certificate(certificate, originalEncoding); + certificates.add(guaranteedEncodedCert); + } + return certificates; + } catch (Asn1DecodingException | Asn1EncodingException e) { + throw new CertificateException("Failed to parse certificates", e); + } + } + + /** + * Parses the provided ByteBuffer to obtain the next certificate in DER encoding. If the buffer + * does not begin with the PEM certificate header then it is returned with the assumption that + * it is already DER encoded. If the buffer does begin with the PEM certificate header then the + * certificate data is read from the buffer until the PEM certificate footer is reached; this + * data is then base64 decoded and returned in a new ByteBuffer. + * + * If the buffer is in PEM format then the position of the buffer is moved to the end of the + * current certificate; if the buffer is already DER encoded then the position of the buffer is + * not modified. + * + * @throws CertificateException if the buffer contains the PEM certificate header but does not + * contain the expected footer. + */ + private static ByteBuffer getNextDEREncodedCertificateBlock(ByteBuffer certificateBuffer) + throws CertificateException { + if (certificateBuffer == null) { + throw new NullPointerException("The certificateBuffer cannot be null"); + } + // if the buffer does not contain enough data for the PEM cert header then just return the + // provided buffer. + if (certificateBuffer.remaining() < BEGIN_CERT_HEADER.length) { + return certificateBuffer; + } + certificateBuffer.mark(); + for (int i = 0; i < BEGIN_CERT_HEADER.length; i++) { + if (certificateBuffer.get() != BEGIN_CERT_HEADER[i]) { + certificateBuffer.reset(); + return certificateBuffer; + } + } + StringBuilder pemEncoding = new StringBuilder(); + while (certificateBuffer.hasRemaining()) { + char encodedChar = (char) certificateBuffer.get(); + // if the current character is a '-' then the beginning of the footer has been reached + if (encodedChar == '-') { + break; + } else if (Character.isWhitespace(encodedChar)) { + continue; + } else { + pemEncoding.append(encodedChar); + } + } + // start from the second index in the certificate footer since the first '-' should have + // been consumed above. + for (int i = 1; i < END_CERT_FOOTER.length; i++) { + if (!certificateBuffer.hasRemaining()) { + throw new CertificateException( + "The provided input contains the PEM certificate header but does not " + + "contain sufficient data for the footer"); + } + if (certificateBuffer.get() != END_CERT_FOOTER[i]) { + throw new CertificateException( + "The provided input contains the PEM certificate header without a " + + "valid certificate footer"); + } + } + byte[] derEncoding = Base64.getDecoder().decode(pemEncoding.toString()); + // consume any trailing whitespace in the byte buffer + int nextEncodedChar = certificateBuffer.position(); + while (certificateBuffer.hasRemaining()) { + char trailingChar = (char) certificateBuffer.get(); + if (Character.isWhitespace(trailingChar)) { + nextEncodedChar++; + } else { + break; + } + } + certificateBuffer.position(nextEncodedChar); + return ByteBuffer.wrap(derEncoding); + } +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/x509/AttributeTypeAndValue.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/x509/AttributeTypeAndValue.java new file mode 100644 index 0000000000..077db232ba --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/x509/AttributeTypeAndValue.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.x509; + +import com.android.apksig.internal.asn1.Asn1Class; +import com.android.apksig.internal.asn1.Asn1Field; +import com.android.apksig.internal.asn1.Asn1OpaqueObject; +import com.android.apksig.internal.asn1.Asn1Type; + +/** + * {@code AttributeTypeAndValue} as specified in RFC 5280. + */ +@Asn1Class(type = Asn1Type.SEQUENCE) +public class AttributeTypeAndValue { + + @Asn1Field(index = 0, type = Asn1Type.OBJECT_IDENTIFIER) + public String attrType; + + @Asn1Field(index = 1, type = Asn1Type.ANY) + public Asn1OpaqueObject attrValue; +}
\ No newline at end of file diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/x509/Certificate.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/x509/Certificate.java new file mode 100644 index 0000000000..70ff6a163c --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/x509/Certificate.java @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.x509; + +import com.android.apksig.internal.asn1.Asn1Class; +import com.android.apksig.internal.asn1.Asn1Field; +import com.android.apksig.internal.asn1.Asn1OpaqueObject; +import com.android.apksig.internal.asn1.Asn1Type; +import com.android.apksig.internal.pkcs7.AlgorithmIdentifier; +import com.android.apksig.internal.pkcs7.IssuerAndSerialNumber; +import com.android.apksig.internal.pkcs7.SignerIdentifier; +import com.android.apksig.internal.util.ByteBufferUtils; +import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate; +import com.android.apksig.internal.util.X509CertificateUtils; + +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import javax.security.auth.x500.X500Principal; + +/** + * X509 {@code Certificate} as specified in RFC 5280. + */ +@Asn1Class(type = Asn1Type.SEQUENCE) +public class Certificate { + @Asn1Field(index = 0, type = Asn1Type.SEQUENCE) + public TBSCertificate certificate; + + @Asn1Field(index = 1, type = Asn1Type.SEQUENCE) + public AlgorithmIdentifier signatureAlgorithm; + + @Asn1Field(index = 2, type = Asn1Type.BIT_STRING) + public ByteBuffer signature; + + public static X509Certificate findCertificate( + Collection<X509Certificate> certs, SignerIdentifier id) { + for (X509Certificate cert : certs) { + if (isMatchingCerticicate(cert, id)) { + return cert; + } + } + return null; + } + + private static boolean isMatchingCerticicate(X509Certificate cert, SignerIdentifier id) { + if (id.issuerAndSerialNumber == null) { + // Android doesn't support any other means of identifying the signing certificate + return false; + } + IssuerAndSerialNumber issuerAndSerialNumber = id.issuerAndSerialNumber; + byte[] encodedIssuer = + ByteBufferUtils.toByteArray(issuerAndSerialNumber.issuer.getEncoded()); + X500Principal idIssuer = new X500Principal(encodedIssuer); + BigInteger idSerialNumber = issuerAndSerialNumber.certificateSerialNumber; + return idSerialNumber.equals(cert.getSerialNumber()) + && idIssuer.equals(cert.getIssuerX500Principal()); + } + + public static List<X509Certificate> parseCertificates( + List<Asn1OpaqueObject> encodedCertificates) throws CertificateException { + if (encodedCertificates.isEmpty()) { + return Collections.emptyList(); + } + + List<X509Certificate> result = new ArrayList<>(encodedCertificates.size()); + for (int i = 0; i < encodedCertificates.size(); i++) { + Asn1OpaqueObject encodedCertificate = encodedCertificates.get(i); + X509Certificate certificate; + byte[] encodedForm = ByteBufferUtils.toByteArray(encodedCertificate.getEncoded()); + try { + certificate = X509CertificateUtils.generateCertificate(encodedForm); + } catch (CertificateException e) { + throw new CertificateException("Failed to parse certificate #" + (i + 1), e); + } + // Wrap the cert so that the result's getEncoded returns exactly the original + // encoded form. Without this, getEncoded may return a different form from what was + // stored in the signature. This is because some X509Certificate(Factory) + // implementations re-encode certificates and/or some implementations of + // X509Certificate.getEncoded() re-encode certificates. + certificate = new GuaranteedEncodedFormX509Certificate(certificate, encodedForm); + result.add(certificate); + } + return result; + } +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/x509/Extension.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/x509/Extension.java new file mode 100644 index 0000000000..bf37c1e824 --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/x509/Extension.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.x509; + +import com.android.apksig.internal.asn1.Asn1Class; +import com.android.apksig.internal.asn1.Asn1Field; +import com.android.apksig.internal.asn1.Asn1Type; + +import java.nio.ByteBuffer; + +/** + * X509 {@code Extension} as specified in RFC 5280. + */ +@Asn1Class(type = Asn1Type.SEQUENCE) +public class Extension { + @Asn1Field(index = 0, type = Asn1Type.OBJECT_IDENTIFIER) + public String extensionID; + + @Asn1Field(index = 1, type = Asn1Type.BOOLEAN, optional = true) + public boolean isCritial = false; + + @Asn1Field(index = 2, type = Asn1Type.OCTET_STRING) + public ByteBuffer extensionValue; +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/x509/Name.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/x509/Name.java new file mode 100644 index 0000000000..08400d6814 --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/x509/Name.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.x509; + +import com.android.apksig.internal.asn1.Asn1Class; +import com.android.apksig.internal.asn1.Asn1Field; +import com.android.apksig.internal.asn1.Asn1Type; + +import java.util.List; + +/** + * X501 {@code Name} as specified in RFC 5280. + */ +@Asn1Class(type = Asn1Type.CHOICE) +public class Name { + + // This field is the RDNSequence specified in RFC 5280. + @Asn1Field(index = 0, type = Asn1Type.SEQUENCE_OF) + public List<RelativeDistinguishedName> relativeDistinguishedNames; +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/x509/RSAPublicKey.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/x509/RSAPublicKey.java new file mode 100644 index 0000000000..521e067c26 --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/x509/RSAPublicKey.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.x509; + +import com.android.apksig.internal.asn1.Asn1Class; +import com.android.apksig.internal.asn1.Asn1Field; +import com.android.apksig.internal.asn1.Asn1Type; + +import java.math.BigInteger; + +/** + * {@code RSAPublicKey} as specified in RFC 3279. + */ +@Asn1Class(type = Asn1Type.SEQUENCE) +public class RSAPublicKey { + @Asn1Field(index = 0, type = Asn1Type.INTEGER) + public BigInteger modulus; + + @Asn1Field(index = 1, type = Asn1Type.INTEGER) + public BigInteger publicExponent; +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/x509/RelativeDistinguishedName.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/x509/RelativeDistinguishedName.java new file mode 100644 index 0000000000..bb89e8d33a --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/x509/RelativeDistinguishedName.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.x509; + +import com.android.apksig.internal.asn1.Asn1Class; +import com.android.apksig.internal.asn1.Asn1Field; +import com.android.apksig.internal.asn1.Asn1Type; + +import java.util.List; + +/** + * {@code RelativeDistinguishedName} as specified in RFC 5280. + */ +@Asn1Class(type = Asn1Type.UNENCODED_CONTAINER) +public class RelativeDistinguishedName { + + @Asn1Field(index = 0, type = Asn1Type.SET_OF) + public List<AttributeTypeAndValue> attributes; +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/x509/SubjectPublicKeyInfo.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/x509/SubjectPublicKeyInfo.java new file mode 100644 index 0000000000..821523761b --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/x509/SubjectPublicKeyInfo.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.x509; + +import com.android.apksig.internal.asn1.Asn1Class; +import com.android.apksig.internal.asn1.Asn1Field; +import com.android.apksig.internal.asn1.Asn1Type; +import com.android.apksig.internal.pkcs7.AlgorithmIdentifier; + +import java.nio.ByteBuffer; + +/** + * {@code SubjectPublicKeyInfo} as specified in RFC 5280. + */ +@Asn1Class(type = Asn1Type.SEQUENCE) +public class SubjectPublicKeyInfo { + @Asn1Field(index = 0, type = Asn1Type.SEQUENCE) + public AlgorithmIdentifier algorithmIdentifier; + + @Asn1Field(index = 1, type = Asn1Type.BIT_STRING) + public ByteBuffer subjectPublicKey; +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/x509/TBSCertificate.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/x509/TBSCertificate.java new file mode 100644 index 0000000000..922f52c205 --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/x509/TBSCertificate.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.x509; + +import com.android.apksig.internal.asn1.Asn1Class; +import com.android.apksig.internal.asn1.Asn1Field; +import com.android.apksig.internal.asn1.Asn1Type; +import com.android.apksig.internal.asn1.Asn1Tagging; +import com.android.apksig.internal.pkcs7.AlgorithmIdentifier; + +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.util.List; + +/** + * To Be Signed Certificate as specified in RFC 5280. + */ +@Asn1Class(type = Asn1Type.SEQUENCE) +public class TBSCertificate { + + @Asn1Field( + index = 0, + type = Asn1Type.INTEGER, + tagging = Asn1Tagging.EXPLICIT, tagNumber = 0) + public int version; + + @Asn1Field(index = 1, type = Asn1Type.INTEGER) + public BigInteger serialNumber; + + @Asn1Field(index = 2, type = Asn1Type.SEQUENCE) + public AlgorithmIdentifier signatureAlgorithm; + + @Asn1Field(index = 3, type = Asn1Type.CHOICE) + public Name issuer; + + @Asn1Field(index = 4, type = Asn1Type.SEQUENCE) + public Validity validity; + + @Asn1Field(index = 5, type = Asn1Type.CHOICE) + public Name subject; + + @Asn1Field(index = 6, type = Asn1Type.SEQUENCE) + public SubjectPublicKeyInfo subjectPublicKeyInfo; + + @Asn1Field(index = 7, + type = Asn1Type.BIT_STRING, + tagging = Asn1Tagging.IMPLICIT, + optional = true, + tagNumber = 1) + public ByteBuffer issuerUniqueID; + + @Asn1Field(index = 8, + type = Asn1Type.BIT_STRING, + tagging = Asn1Tagging.IMPLICIT, + optional = true, + tagNumber = 2) + public ByteBuffer subjectUniqueID; + + @Asn1Field(index = 9, + type = Asn1Type.SEQUENCE_OF, + tagging = Asn1Tagging.EXPLICIT, + optional = true, + tagNumber = 3) + public List<Extension> extensions; +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/x509/Time.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/x509/Time.java new file mode 100644 index 0000000000..def2ee8947 --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/x509/Time.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.x509; + +import com.android.apksig.internal.asn1.Asn1Class; +import com.android.apksig.internal.asn1.Asn1Field; +import com.android.apksig.internal.asn1.Asn1Type; + +/** + * {@code Time} as specified in RFC 5280. + */ +@Asn1Class(type = Asn1Type.CHOICE) +public class Time { + + @Asn1Field(type = Asn1Type.UTC_TIME) + public String utcTime; + + @Asn1Field(type = Asn1Type.GENERALIZED_TIME) + public String generalizedTime; +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/x509/Validity.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/x509/Validity.java new file mode 100644 index 0000000000..df9acb3f36 --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/x509/Validity.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.x509; + +import com.android.apksig.internal.asn1.Asn1Class; +import com.android.apksig.internal.asn1.Asn1Field; +import com.android.apksig.internal.asn1.Asn1Type; + +/** + * {@code Validity} as specified in RFC 5280. + */ +@Asn1Class(type = Asn1Type.SEQUENCE) +public class Validity { + + @Asn1Field(index = 0, type = Asn1Type.CHOICE) + public Time notBefore; + + @Asn1Field(index = 1, type = Asn1Type.CHOICE) + public Time notAfter; +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/zip/CentralDirectoryRecord.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/zip/CentralDirectoryRecord.java new file mode 100644 index 0000000000..d2f444ddcd --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/zip/CentralDirectoryRecord.java @@ -0,0 +1,304 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.zip; + +import com.android.apksig.zip.ZipFormatException; +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.charset.StandardCharsets; +import java.util.Comparator; + +/** + * ZIP Central Directory (CD) Record. + */ +public class CentralDirectoryRecord { + + /** + * Comparator which compares records by the offset of the corresponding Local File Header in the + * archive. + */ + public static final Comparator<CentralDirectoryRecord> BY_LOCAL_FILE_HEADER_OFFSET_COMPARATOR = + new ByLocalFileHeaderOffsetComparator(); + + private static final int RECORD_SIGNATURE = 0x02014b50; + private static final int HEADER_SIZE_BYTES = 46; + + private static final int GP_FLAGS_OFFSET = 8; + private static final int LOCAL_FILE_HEADER_OFFSET_OFFSET = 42; + private static final int NAME_OFFSET = HEADER_SIZE_BYTES; + + private final ByteBuffer mData; + private final short mGpFlags; + private final short mCompressionMethod; + private final int mLastModificationTime; + private final int mLastModificationDate; + private final long mCrc32; + private final long mCompressedSize; + private final long mUncompressedSize; + private final long mLocalFileHeaderOffset; + private final String mName; + private final int mNameSizeBytes; + + private CentralDirectoryRecord( + ByteBuffer data, + short gpFlags, + short compressionMethod, + int lastModificationTime, + int lastModificationDate, + long crc32, + long compressedSize, + long uncompressedSize, + long localFileHeaderOffset, + String name, + int nameSizeBytes) { + mData = data; + mGpFlags = gpFlags; + mCompressionMethod = compressionMethod; + mLastModificationDate = lastModificationDate; + mLastModificationTime = lastModificationTime; + mCrc32 = crc32; + mCompressedSize = compressedSize; + mUncompressedSize = uncompressedSize; + mLocalFileHeaderOffset = localFileHeaderOffset; + mName = name; + mNameSizeBytes = nameSizeBytes; + } + + public int getSize() { + return mData.remaining(); + } + + public String getName() { + return mName; + } + + public int getNameSizeBytes() { + return mNameSizeBytes; + } + + public short getGpFlags() { + return mGpFlags; + } + + public short getCompressionMethod() { + return mCompressionMethod; + } + + public int getLastModificationTime() { + return mLastModificationTime; + } + + public int getLastModificationDate() { + return mLastModificationDate; + } + + public long getCrc32() { + return mCrc32; + } + + public long getCompressedSize() { + return mCompressedSize; + } + + public long getUncompressedSize() { + return mUncompressedSize; + } + + public long getLocalFileHeaderOffset() { + return mLocalFileHeaderOffset; + } + + /** + * Returns the Central Directory Record starting at the current position of the provided buffer + * and advances the buffer's position immediately past the end of the record. + */ + public static CentralDirectoryRecord getRecord(ByteBuffer buf) throws ZipFormatException { + ZipUtils.assertByteOrderLittleEndian(buf); + if (buf.remaining() < HEADER_SIZE_BYTES) { + throw new ZipFormatException( + "Input too short. Need at least: " + HEADER_SIZE_BYTES + + " bytes, available: " + buf.remaining() + " bytes", + new BufferUnderflowException()); + } + int originalPosition = buf.position(); + int recordSignature = buf.getInt(); + if (recordSignature != RECORD_SIGNATURE) { + throw new ZipFormatException( + "Not a Central Directory record. Signature: 0x" + + Long.toHexString(recordSignature & 0xffffffffL)); + } + buf.position(originalPosition + GP_FLAGS_OFFSET); + short gpFlags = buf.getShort(); + short compressionMethod = buf.getShort(); + int lastModificationTime = ZipUtils.getUnsignedInt16(buf); + int lastModificationDate = ZipUtils.getUnsignedInt16(buf); + long crc32 = ZipUtils.getUnsignedInt32(buf); + long compressedSize = ZipUtils.getUnsignedInt32(buf); + long uncompressedSize = ZipUtils.getUnsignedInt32(buf); + int nameSize = ZipUtils.getUnsignedInt16(buf); + int extraSize = ZipUtils.getUnsignedInt16(buf); + int commentSize = ZipUtils.getUnsignedInt16(buf); + buf.position(originalPosition + LOCAL_FILE_HEADER_OFFSET_OFFSET); + long localFileHeaderOffset = ZipUtils.getUnsignedInt32(buf); + buf.position(originalPosition); + int recordSize = HEADER_SIZE_BYTES + nameSize + extraSize + commentSize; + if (recordSize > buf.remaining()) { + throw new ZipFormatException( + "Input too short. Need: " + recordSize + " bytes, available: " + + buf.remaining() + " bytes", + new BufferUnderflowException()); + } + String name = getName(buf, originalPosition + NAME_OFFSET, nameSize); + buf.position(originalPosition); + int originalLimit = buf.limit(); + int recordEndInBuf = originalPosition + recordSize; + ByteBuffer recordBuf; + try { + buf.limit(recordEndInBuf); + recordBuf = buf.slice(); + } finally { + buf.limit(originalLimit); + } + // Consume this record + buf.position(recordEndInBuf); + return new CentralDirectoryRecord( + recordBuf, + gpFlags, + compressionMethod, + lastModificationTime, + lastModificationDate, + crc32, + compressedSize, + uncompressedSize, + localFileHeaderOffset, + name, + nameSize); + } + + public void copyTo(ByteBuffer output) { + output.put(mData.slice()); + } + + public CentralDirectoryRecord createWithModifiedLocalFileHeaderOffset( + long localFileHeaderOffset) { + ByteBuffer result = ByteBuffer.allocate(mData.remaining()); + result.put(mData.slice()); + result.flip(); + result.order(ByteOrder.LITTLE_ENDIAN); + ZipUtils.setUnsignedInt32(result, LOCAL_FILE_HEADER_OFFSET_OFFSET, localFileHeaderOffset); + return new CentralDirectoryRecord( + result, + mGpFlags, + mCompressionMethod, + mLastModificationTime, + mLastModificationDate, + mCrc32, + mCompressedSize, + mUncompressedSize, + localFileHeaderOffset, + mName, + mNameSizeBytes); + } + + public static CentralDirectoryRecord createWithDeflateCompressedData( + String name, + int lastModifiedTime, + int lastModifiedDate, + long crc32, + long compressedSize, + long uncompressedSize, + long localFileHeaderOffset) { + byte[] nameBytes = name.getBytes(StandardCharsets.UTF_8); + short gpFlags = ZipUtils.GP_FLAG_EFS; // UTF-8 character encoding used for entry name + short compressionMethod = ZipUtils.COMPRESSION_METHOD_DEFLATED; + int recordSize = HEADER_SIZE_BYTES + nameBytes.length; + ByteBuffer result = ByteBuffer.allocate(recordSize); + result.order(ByteOrder.LITTLE_ENDIAN); + result.putInt(RECORD_SIGNATURE); + ZipUtils.putUnsignedInt16(result, 0x14); // Version made by + ZipUtils.putUnsignedInt16(result, 0x14); // Minimum version needed to extract + result.putShort(gpFlags); + result.putShort(compressionMethod); + ZipUtils.putUnsignedInt16(result, lastModifiedTime); + ZipUtils.putUnsignedInt16(result, lastModifiedDate); + ZipUtils.putUnsignedInt32(result, crc32); + ZipUtils.putUnsignedInt32(result, compressedSize); + ZipUtils.putUnsignedInt32(result, uncompressedSize); + ZipUtils.putUnsignedInt16(result, nameBytes.length); + ZipUtils.putUnsignedInt16(result, 0); // Extra field length + ZipUtils.putUnsignedInt16(result, 0); // File comment length + ZipUtils.putUnsignedInt16(result, 0); // Disk number + ZipUtils.putUnsignedInt16(result, 0); // Internal file attributes + ZipUtils.putUnsignedInt32(result, 0); // External file attributes + ZipUtils.putUnsignedInt32(result, localFileHeaderOffset); + result.put(nameBytes); + + if (result.hasRemaining()) { + throw new RuntimeException("pos: " + result.position() + ", limit: " + result.limit()); + } + result.flip(); + return new CentralDirectoryRecord( + result, + gpFlags, + compressionMethod, + lastModifiedTime, + lastModifiedDate, + crc32, + compressedSize, + uncompressedSize, + localFileHeaderOffset, + name, + nameBytes.length); + } + + static String getName(ByteBuffer record, int position, int nameLengthBytes) { + byte[] nameBytes; + int nameBytesOffset; + if (record.hasArray()) { + nameBytes = record.array(); + nameBytesOffset = record.arrayOffset() + position; + } else { + nameBytes = new byte[nameLengthBytes]; + nameBytesOffset = 0; + int originalPosition = record.position(); + try { + record.position(position); + record.get(nameBytes); + } finally { + record.position(originalPosition); + } + } + return new String(nameBytes, nameBytesOffset, nameLengthBytes, StandardCharsets.UTF_8); + } + + private static class ByLocalFileHeaderOffsetComparator + implements Comparator<CentralDirectoryRecord> { + @Override + public int compare(CentralDirectoryRecord r1, CentralDirectoryRecord r2) { + long offset1 = r1.getLocalFileHeaderOffset(); + long offset2 = r2.getLocalFileHeaderOffset(); + if (offset1 > offset2) { + return 1; + } else if (offset1 < offset2) { + return -1; + } else { + return 0; + } + } + } +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/zip/EocdRecord.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/zip/EocdRecord.java new file mode 100644 index 0000000000..d2000b42dd --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/zip/EocdRecord.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.zip; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/** + * ZIP End of Central Directory record. + */ +public class EocdRecord { + private static final int CD_RECORD_COUNT_ON_DISK_OFFSET = 8; + private static final int CD_RECORD_COUNT_TOTAL_OFFSET = 10; + private static final int CD_SIZE_OFFSET = 12; + private static final int CD_OFFSET_OFFSET = 16; + + public static ByteBuffer createWithModifiedCentralDirectoryInfo( + ByteBuffer original, + int centralDirectoryRecordCount, + long centralDirectorySizeBytes, + long centralDirectoryOffset) { + ByteBuffer result = ByteBuffer.allocate(original.remaining()); + result.order(ByteOrder.LITTLE_ENDIAN); + result.put(original.slice()); + result.flip(); + ZipUtils.setUnsignedInt16( + result, CD_RECORD_COUNT_ON_DISK_OFFSET, centralDirectoryRecordCount); + ZipUtils.setUnsignedInt16( + result, CD_RECORD_COUNT_TOTAL_OFFSET, centralDirectoryRecordCount); + ZipUtils.setUnsignedInt32(result, CD_SIZE_OFFSET, centralDirectorySizeBytes); + ZipUtils.setUnsignedInt32(result, CD_OFFSET_OFFSET, centralDirectoryOffset); + return result; + } + + public static ByteBuffer createWithPaddedComment(ByteBuffer original, int padding) { + ByteBuffer result = ByteBuffer.allocate((int) original.remaining() + padding); + result.order(ByteOrder.LITTLE_ENDIAN); + result.put(original.slice()); + result.rewind(); + ZipUtils.updateZipEocdCommentLen(result); + return result; + } +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/zip/LocalFileRecord.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/zip/LocalFileRecord.java new file mode 100644 index 0000000000..50ce386aa7 --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/zip/LocalFileRecord.java @@ -0,0 +1,543 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.zip; + +import com.android.apksig.internal.util.ByteBufferSink; +import com.android.apksig.util.DataSink; +import com.android.apksig.util.DataSource; +import com.android.apksig.zip.ZipFormatException; +import java.io.Closeable; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.charset.StandardCharsets; +import java.util.zip.DataFormatException; +import java.util.zip.Inflater; + +/** + * ZIP Local File record. + * + * <p>The record consists of the Local File Header, file data, and (if present) Data Descriptor. + */ +public class LocalFileRecord { + private static final int RECORD_SIGNATURE = 0x04034b50; + private static final int HEADER_SIZE_BYTES = 30; + + private static final int GP_FLAGS_OFFSET = 6; + private static final int CRC32_OFFSET = 14; + private static final int COMPRESSED_SIZE_OFFSET = 18; + private static final int UNCOMPRESSED_SIZE_OFFSET = 22; + private static final int NAME_LENGTH_OFFSET = 26; + private static final int EXTRA_LENGTH_OFFSET = 28; + private static final int NAME_OFFSET = HEADER_SIZE_BYTES; + + private static final int DATA_DESCRIPTOR_SIZE_BYTES_WITHOUT_SIGNATURE = 12; + private static final int DATA_DESCRIPTOR_SIGNATURE = 0x08074b50; + + private final String mName; + private final int mNameSizeBytes; + private final ByteBuffer mExtra; + + private final long mStartOffsetInArchive; + private final long mSize; + + private final int mDataStartOffset; + private final long mDataSize; + private final boolean mDataCompressed; + private final long mUncompressedDataSize; + + private LocalFileRecord( + String name, + int nameSizeBytes, + ByteBuffer extra, + long startOffsetInArchive, + long size, + int dataStartOffset, + long dataSize, + boolean dataCompressed, + long uncompressedDataSize) { + mName = name; + mNameSizeBytes = nameSizeBytes; + mExtra = extra; + mStartOffsetInArchive = startOffsetInArchive; + mSize = size; + mDataStartOffset = dataStartOffset; + mDataSize = dataSize; + mDataCompressed = dataCompressed; + mUncompressedDataSize = uncompressedDataSize; + } + + public String getName() { + return mName; + } + + public ByteBuffer getExtra() { + return (mExtra.capacity() > 0) ? mExtra.slice() : mExtra; + } + + public int getExtraFieldStartOffsetInsideRecord() { + return HEADER_SIZE_BYTES + mNameSizeBytes; + } + + public long getStartOffsetInArchive() { + return mStartOffsetInArchive; + } + + public int getDataStartOffsetInRecord() { + return mDataStartOffset; + } + + /** + * Returns the size (in bytes) of this record. + */ + public long getSize() { + return mSize; + } + + /** + * Returns {@code true} if this record's file data is stored in compressed form. + */ + public boolean isDataCompressed() { + return mDataCompressed; + } + + /** + * Returns the Local File record starting at the current position of the provided buffer + * and advances the buffer's position immediately past the end of the record. The record + * consists of the Local File Header, data, and (if present) Data Descriptor. + */ + public static LocalFileRecord getRecord( + DataSource apk, + CentralDirectoryRecord cdRecord, + long cdStartOffset) throws ZipFormatException, IOException { + return getRecord( + apk, + cdRecord, + cdStartOffset, + true, // obtain extra field contents + true // include Data Descriptor (if present) + ); + } + + /** + * Returns the Local File record starting at the current position of the provided buffer + * and advances the buffer's position immediately past the end of the record. The record + * consists of the Local File Header, data, and (if present) Data Descriptor. + */ + private static LocalFileRecord getRecord( + DataSource apk, + CentralDirectoryRecord cdRecord, + long cdStartOffset, + boolean extraFieldContentsNeeded, + boolean dataDescriptorIncluded) throws ZipFormatException, IOException { + // IMPLEMENTATION NOTE: This method attempts to mimic the behavior of Android platform + // exhibited when reading an APK for the purposes of verifying its signatures. + + String entryName = cdRecord.getName(); + int cdRecordEntryNameSizeBytes = cdRecord.getNameSizeBytes(); + int headerSizeWithName = HEADER_SIZE_BYTES + cdRecordEntryNameSizeBytes; + long headerStartOffset = cdRecord.getLocalFileHeaderOffset(); + long headerEndOffset = headerStartOffset + headerSizeWithName; + if (headerEndOffset > cdStartOffset) { + throw new ZipFormatException( + "Local File Header of " + entryName + " extends beyond start of Central" + + " Directory. LFH end: " + headerEndOffset + + ", CD start: " + cdStartOffset); + } + ByteBuffer header; + try { + header = apk.getByteBuffer(headerStartOffset, headerSizeWithName); + } catch (IOException e) { + throw new IOException("Failed to read Local File Header of " + entryName, e); + } + header.order(ByteOrder.LITTLE_ENDIAN); + + int recordSignature = header.getInt(); + if (recordSignature != RECORD_SIGNATURE) { + throw new ZipFormatException( + "Not a Local File Header record for entry " + entryName + ". Signature: 0x" + + Long.toHexString(recordSignature & 0xffffffffL)); + } + short gpFlags = header.getShort(GP_FLAGS_OFFSET); + boolean dataDescriptorUsed = (gpFlags & ZipUtils.GP_FLAG_DATA_DESCRIPTOR_USED) != 0; + boolean cdDataDescriptorUsed = + (cdRecord.getGpFlags() & ZipUtils.GP_FLAG_DATA_DESCRIPTOR_USED) != 0; + if (dataDescriptorUsed != cdDataDescriptorUsed) { + throw new ZipFormatException( + "Data Descriptor presence mismatch between Local File Header and Central" + + " Directory for entry " + entryName + + ". LFH: " + dataDescriptorUsed + ", CD: " + cdDataDescriptorUsed); + } + long uncompressedDataCrc32FromCdRecord = cdRecord.getCrc32(); + long compressedDataSizeFromCdRecord = cdRecord.getCompressedSize(); + long uncompressedDataSizeFromCdRecord = cdRecord.getUncompressedSize(); + if (!dataDescriptorUsed) { + long crc32 = ZipUtils.getUnsignedInt32(header, CRC32_OFFSET); + if (crc32 != uncompressedDataCrc32FromCdRecord) { + throw new ZipFormatException( + "CRC-32 mismatch between Local File Header and Central Directory for entry " + + entryName + ". LFH: " + crc32 + + ", CD: " + uncompressedDataCrc32FromCdRecord); + } + long compressedSize = ZipUtils.getUnsignedInt32(header, COMPRESSED_SIZE_OFFSET); + if (compressedSize != compressedDataSizeFromCdRecord) { + throw new ZipFormatException( + "Compressed size mismatch between Local File Header and Central Directory" + + " for entry " + entryName + ". LFH: " + compressedSize + + ", CD: " + compressedDataSizeFromCdRecord); + } + long uncompressedSize = ZipUtils.getUnsignedInt32(header, UNCOMPRESSED_SIZE_OFFSET); + if (uncompressedSize != uncompressedDataSizeFromCdRecord) { + throw new ZipFormatException( + "Uncompressed size mismatch between Local File Header and Central Directory" + + " for entry " + entryName + ". LFH: " + uncompressedSize + + ", CD: " + uncompressedDataSizeFromCdRecord); + } + } + int nameLength = ZipUtils.getUnsignedInt16(header, NAME_LENGTH_OFFSET); + if (nameLength > cdRecordEntryNameSizeBytes) { + throw new ZipFormatException( + "Name mismatch between Local File Header and Central Directory for entry" + + entryName + ". LFH: " + nameLength + + " bytes, CD: " + cdRecordEntryNameSizeBytes + " bytes"); + } + String name = CentralDirectoryRecord.getName(header, NAME_OFFSET, nameLength); + if (!entryName.equals(name)) { + throw new ZipFormatException( + "Name mismatch between Local File Header and Central Directory. LFH: \"" + + name + "\", CD: \"" + entryName + "\""); + } + int extraLength = ZipUtils.getUnsignedInt16(header, EXTRA_LENGTH_OFFSET); + long dataStartOffset = headerStartOffset + HEADER_SIZE_BYTES + nameLength + extraLength; + long dataSize; + boolean compressed = + (cdRecord.getCompressionMethod() != ZipUtils.COMPRESSION_METHOD_STORED); + if (compressed) { + dataSize = compressedDataSizeFromCdRecord; + } else { + dataSize = uncompressedDataSizeFromCdRecord; + } + long dataEndOffset = dataStartOffset + dataSize; + if (dataEndOffset > cdStartOffset) { + throw new ZipFormatException( + "Local File Header data of " + entryName + " overlaps with Central Directory" + + ". LFH data start: " + dataStartOffset + + ", LFH data end: " + dataEndOffset + ", CD start: " + cdStartOffset); + } + + ByteBuffer extra = EMPTY_BYTE_BUFFER; + if ((extraFieldContentsNeeded) && (extraLength > 0)) { + extra = apk.getByteBuffer( + headerStartOffset + HEADER_SIZE_BYTES + nameLength, extraLength); + } + + long recordEndOffset = dataEndOffset; + // Include the Data Descriptor (if requested and present) into the record. + if ((dataDescriptorIncluded) && ((gpFlags & ZipUtils.GP_FLAG_DATA_DESCRIPTOR_USED) != 0)) { + // The record's data is supposed to be followed by the Data Descriptor. Unfortunately, + // the descriptor's size is not known in advance because the spec lets the signature + // field (the first four bytes) be omitted. Thus, there's no 100% reliable way to tell + // how long the Data Descriptor record is. Most parsers (including Android) check + // whether the first four bytes look like Data Descriptor record signature and, if so, + // assume that it is indeed the record's signature. However, this is the wrong + // conclusion if the record's CRC-32 (next field after the signature) has the same value + // as the signature. In any case, we're doing what Android is doing. + long dataDescriptorEndOffset = + dataEndOffset + DATA_DESCRIPTOR_SIZE_BYTES_WITHOUT_SIGNATURE; + if (dataDescriptorEndOffset > cdStartOffset) { + throw new ZipFormatException( + "Data Descriptor of " + entryName + " overlaps with Central Directory" + + ". Data Descriptor end: " + dataEndOffset + + ", CD start: " + cdStartOffset); + } + ByteBuffer dataDescriptorPotentialSig = apk.getByteBuffer(dataEndOffset, 4); + dataDescriptorPotentialSig.order(ByteOrder.LITTLE_ENDIAN); + if (dataDescriptorPotentialSig.getInt() == DATA_DESCRIPTOR_SIGNATURE) { + dataDescriptorEndOffset += 4; + if (dataDescriptorEndOffset > cdStartOffset) { + throw new ZipFormatException( + "Data Descriptor of " + entryName + " overlaps with Central Directory" + + ". Data Descriptor end: " + dataEndOffset + + ", CD start: " + cdStartOffset); + } + } + recordEndOffset = dataDescriptorEndOffset; + } + + long recordSize = recordEndOffset - headerStartOffset; + int dataStartOffsetInRecord = HEADER_SIZE_BYTES + nameLength + extraLength; + + return new LocalFileRecord( + entryName, + cdRecordEntryNameSizeBytes, + extra, + headerStartOffset, + recordSize, + dataStartOffsetInRecord, + dataSize, + compressed, + uncompressedDataSizeFromCdRecord); + } + + /** + * Outputs this record and returns returns the number of bytes output. + */ + public long outputRecord(DataSource sourceApk, DataSink output) throws IOException { + long size = getSize(); + sourceApk.feed(getStartOffsetInArchive(), size, output); + return size; + } + + /** + * Outputs this record, replacing its extra field with the provided one, and returns returns the + * number of bytes output. + */ + public long outputRecordWithModifiedExtra( + DataSource sourceApk, + ByteBuffer extra, + DataSink output) throws IOException { + long recordStartOffsetInSource = getStartOffsetInArchive(); + int extraStartOffsetInRecord = getExtraFieldStartOffsetInsideRecord(); + int extraSizeBytes = extra.remaining(); + int headerSize = extraStartOffsetInRecord + extraSizeBytes; + ByteBuffer header = ByteBuffer.allocate(headerSize); + header.order(ByteOrder.LITTLE_ENDIAN); + sourceApk.copyTo(recordStartOffsetInSource, extraStartOffsetInRecord, header); + header.put(extra.slice()); + header.flip(); + ZipUtils.setUnsignedInt16(header, EXTRA_LENGTH_OFFSET, extraSizeBytes); + + long outputByteCount = header.remaining(); + output.consume(header); + long remainingRecordSize = getSize() - mDataStartOffset; + sourceApk.feed(recordStartOffsetInSource + mDataStartOffset, remainingRecordSize, output); + outputByteCount += remainingRecordSize; + return outputByteCount; + } + + /** + * Outputs the specified Local File Header record with its data and returns the number of bytes + * output. + */ + public static long outputRecordWithDeflateCompressedData( + String name, + int lastModifiedTime, + int lastModifiedDate, + byte[] compressedData, + long crc32, + long uncompressedSize, + DataSink output) throws IOException { + byte[] nameBytes = name.getBytes(StandardCharsets.UTF_8); + int recordSize = HEADER_SIZE_BYTES + nameBytes.length; + ByteBuffer result = ByteBuffer.allocate(recordSize); + result.order(ByteOrder.LITTLE_ENDIAN); + result.putInt(RECORD_SIGNATURE); + ZipUtils.putUnsignedInt16(result, 0x14); // Minimum version needed to extract + result.putShort(ZipUtils.GP_FLAG_EFS); // General purpose flag: UTF-8 encoded name + result.putShort(ZipUtils.COMPRESSION_METHOD_DEFLATED); + ZipUtils.putUnsignedInt16(result, lastModifiedTime); + ZipUtils.putUnsignedInt16(result, lastModifiedDate); + ZipUtils.putUnsignedInt32(result, crc32); + ZipUtils.putUnsignedInt32(result, compressedData.length); + ZipUtils.putUnsignedInt32(result, uncompressedSize); + ZipUtils.putUnsignedInt16(result, nameBytes.length); + ZipUtils.putUnsignedInt16(result, 0); // Extra field length + result.put(nameBytes); + if (result.hasRemaining()) { + throw new RuntimeException("pos: " + result.position() + ", limit: " + result.limit()); + } + result.flip(); + + long outputByteCount = result.remaining(); + output.consume(result); + outputByteCount += compressedData.length; + output.consume(compressedData, 0, compressedData.length); + return outputByteCount; + } + + private static final ByteBuffer EMPTY_BYTE_BUFFER = ByteBuffer.allocate(0); + + /** + * Sends uncompressed data of this record into the the provided data sink. + */ + public void outputUncompressedData( + DataSource lfhSection, + DataSink sink) throws IOException, ZipFormatException { + long dataStartOffsetInArchive = mStartOffsetInArchive + mDataStartOffset; + try { + if (mDataCompressed) { + try (InflateSinkAdapter inflateAdapter = new InflateSinkAdapter(sink)) { + lfhSection.feed(dataStartOffsetInArchive, mDataSize, inflateAdapter); + long actualUncompressedSize = inflateAdapter.getOutputByteCount(); + if (actualUncompressedSize != mUncompressedDataSize) { + throw new ZipFormatException( + "Unexpected size of uncompressed data of " + mName + + ". Expected: " + mUncompressedDataSize + " bytes" + + ", actual: " + actualUncompressedSize + " bytes"); + } + } catch (IOException e) { + if (e.getCause() instanceof DataFormatException) { + throw new ZipFormatException("Data of entry " + mName + " malformed", e); + } + throw e; + } + } else { + lfhSection.feed(dataStartOffsetInArchive, mDataSize, sink); + // No need to check whether output size is as expected because DataSource.feed is + // guaranteed to output exactly the number of bytes requested. + } + } catch (IOException e) { + throw new IOException( + "Failed to read data of " + ((mDataCompressed) ? "compressed" : "uncompressed") + + " entry " + mName, + e); + } + // Interestingly, Android doesn't check that uncompressed data's CRC-32 is as expected. We + // thus don't check either. + } + + /** + * Sends uncompressed data pointed to by the provided ZIP Central Directory (CD) record into the + * provided data sink. + */ + public static void outputUncompressedData( + DataSource source, + CentralDirectoryRecord cdRecord, + long cdStartOffsetInArchive, + DataSink sink) throws ZipFormatException, IOException { + // IMPLEMENTATION NOTE: This method attempts to mimic the behavior of Android platform + // exhibited when reading an APK for the purposes of verifying its signatures. + // When verifying an APK, Android doesn't care reading the extra field or the Data + // Descriptor. + LocalFileRecord lfhRecord = + getRecord( + source, + cdRecord, + cdStartOffsetInArchive, + false, // don't care about the extra field + false // don't read the Data Descriptor + ); + lfhRecord.outputUncompressedData(source, sink); + } + + /** + * Returns the uncompressed data pointed to by the provided ZIP Central Directory (CD) record. + */ + public static byte[] getUncompressedData( + DataSource source, + CentralDirectoryRecord cdRecord, + long cdStartOffsetInArchive) throws ZipFormatException, IOException { + if (cdRecord.getUncompressedSize() > Integer.MAX_VALUE) { + throw new IOException( + cdRecord.getName() + " too large: " + cdRecord.getUncompressedSize()); + } + byte[] result = null; + try { + result = new byte[(int) cdRecord.getUncompressedSize()]; + } catch (OutOfMemoryError e) { + throw new IOException( + cdRecord.getName() + " too large: " + cdRecord.getUncompressedSize(), e); + } + ByteBuffer resultBuf = ByteBuffer.wrap(result); + ByteBufferSink resultSink = new ByteBufferSink(resultBuf); + outputUncompressedData( + source, + cdRecord, + cdStartOffsetInArchive, + resultSink); + return result; + } + + /** + * {@link DataSink} which inflates received data and outputs the deflated data into the provided + * delegate sink. + */ + private static class InflateSinkAdapter implements DataSink, Closeable { + private final DataSink mDelegate; + + private Inflater mInflater = new Inflater(true); + private byte[] mOutputBuffer; + private byte[] mInputBuffer; + private long mOutputByteCount; + private boolean mClosed; + + private InflateSinkAdapter(DataSink delegate) { + mDelegate = delegate; + } + + @Override + public void consume(byte[] buf, int offset, int length) throws IOException { + checkNotClosed(); + mInflater.setInput(buf, offset, length); + if (mOutputBuffer == null) { + mOutputBuffer = new byte[65536]; + } + while (!mInflater.finished()) { + int outputChunkSize; + try { + outputChunkSize = mInflater.inflate(mOutputBuffer); + } catch (DataFormatException e) { + throw new IOException("Failed to inflate data", e); + } + if (outputChunkSize == 0) { + return; + } + mDelegate.consume(mOutputBuffer, 0, outputChunkSize); + mOutputByteCount += outputChunkSize; + } + } + + @Override + public void consume(ByteBuffer buf) throws IOException { + checkNotClosed(); + if (buf.hasArray()) { + consume(buf.array(), buf.arrayOffset() + buf.position(), buf.remaining()); + buf.position(buf.limit()); + } else { + if (mInputBuffer == null) { + mInputBuffer = new byte[65536]; + } + while (buf.hasRemaining()) { + int chunkSize = Math.min(buf.remaining(), mInputBuffer.length); + buf.get(mInputBuffer, 0, chunkSize); + consume(mInputBuffer, 0, chunkSize); + } + } + } + + public long getOutputByteCount() { + return mOutputByteCount; + } + + @Override + public void close() throws IOException { + mClosed = true; + mInputBuffer = null; + mOutputBuffer = null; + if (mInflater != null) { + mInflater.end(); + mInflater = null; + } + } + + private void checkNotClosed() { + if (mClosed) { + throw new IllegalStateException("Closed"); + } + } + } +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/internal/zip/ZipUtils.java b/platform/android/java/editor/src/main/java/com/android/apksig/internal/zip/ZipUtils.java new file mode 100644 index 0000000000..1c2e82cdab --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/internal/zip/ZipUtils.java @@ -0,0 +1,385 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.zip; + +import com.android.apksig.apk.ApkFormatException; +import com.android.apksig.internal.util.Pair; +import com.android.apksig.util.DataSource; +import com.android.apksig.zip.ZipFormatException; +import com.android.apksig.zip.ZipSections; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.List; +import java.util.zip.CRC32; +import java.util.zip.Deflater; + +/** + * Assorted ZIP format helpers. + * + * <p>NOTE: Most helper methods operating on {@code ByteBuffer} instances expect that the byte + * order of these buffers is little-endian. + */ +public abstract class ZipUtils { + private ZipUtils() {} + + public static final short COMPRESSION_METHOD_STORED = 0; + public static final short COMPRESSION_METHOD_DEFLATED = 8; + + public static final short GP_FLAG_DATA_DESCRIPTOR_USED = 0x08; + public static final short GP_FLAG_EFS = 0x0800; + + private static final int ZIP_EOCD_REC_MIN_SIZE = 22; + private static final int ZIP_EOCD_REC_SIG = 0x06054b50; + private static final int ZIP_EOCD_CENTRAL_DIR_TOTAL_RECORD_COUNT_OFFSET = 10; + private static final int ZIP_EOCD_CENTRAL_DIR_SIZE_FIELD_OFFSET = 12; + private static final int ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET = 16; + private static final int ZIP_EOCD_COMMENT_LENGTH_FIELD_OFFSET = 20; + + private static final int UINT16_MAX_VALUE = 0xffff; + + /** + * Sets the offset of the start of the ZIP Central Directory in the archive. + * + * <p>NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian. + */ + public static void setZipEocdCentralDirectoryOffset( + ByteBuffer zipEndOfCentralDirectory, long offset) { + assertByteOrderLittleEndian(zipEndOfCentralDirectory); + setUnsignedInt32( + zipEndOfCentralDirectory, + zipEndOfCentralDirectory.position() + ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET, + offset); + } + + /** + * Sets the length of EOCD comment. + * + * <p>NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian. + */ + public static void updateZipEocdCommentLen(ByteBuffer zipEndOfCentralDirectory) { + assertByteOrderLittleEndian(zipEndOfCentralDirectory); + int commentLen = zipEndOfCentralDirectory.remaining() - ZIP_EOCD_REC_MIN_SIZE; + setUnsignedInt16( + zipEndOfCentralDirectory, + zipEndOfCentralDirectory.position() + ZIP_EOCD_COMMENT_LENGTH_FIELD_OFFSET, + commentLen); + } + + /** + * Returns the offset of the start of the ZIP Central Directory in the archive. + * + * <p>NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian. + */ + public static long getZipEocdCentralDirectoryOffset(ByteBuffer zipEndOfCentralDirectory) { + assertByteOrderLittleEndian(zipEndOfCentralDirectory); + return getUnsignedInt32( + zipEndOfCentralDirectory, + zipEndOfCentralDirectory.position() + ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET); + } + + /** + * Returns the size (in bytes) of the ZIP Central Directory. + * + * <p>NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian. + */ + public static long getZipEocdCentralDirectorySizeBytes(ByteBuffer zipEndOfCentralDirectory) { + assertByteOrderLittleEndian(zipEndOfCentralDirectory); + return getUnsignedInt32( + zipEndOfCentralDirectory, + zipEndOfCentralDirectory.position() + ZIP_EOCD_CENTRAL_DIR_SIZE_FIELD_OFFSET); + } + + /** + * Returns the total number of records in ZIP Central Directory. + * + * <p>NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian. + */ + public static int getZipEocdCentralDirectoryTotalRecordCount( + ByteBuffer zipEndOfCentralDirectory) { + assertByteOrderLittleEndian(zipEndOfCentralDirectory); + return getUnsignedInt16( + zipEndOfCentralDirectory, + zipEndOfCentralDirectory.position() + + ZIP_EOCD_CENTRAL_DIR_TOTAL_RECORD_COUNT_OFFSET); + } + + /** + * Returns the ZIP End of Central Directory record of the provided ZIP file. + * + * @return contents of the ZIP End of Central Directory record and the record's offset in the + * file or {@code null} if the file does not contain the record. + * + * @throws IOException if an I/O error occurs while reading the file. + */ + public static Pair<ByteBuffer, Long> findZipEndOfCentralDirectoryRecord(DataSource zip) + throws IOException { + // ZIP End of Central Directory (EOCD) record is located at the very end of the ZIP archive. + // The record can be identified by its 4-byte signature/magic which is located at the very + // beginning of the record. A complication is that the record is variable-length because of + // the comment field. + // The algorithm for locating the ZIP EOCD record is as follows. We search backwards from + // end of the buffer for the EOCD record signature. Whenever we find a signature, we check + // the candidate record's comment length is such that the remainder of the record takes up + // exactly the remaining bytes in the buffer. The search is bounded because the maximum + // size of the comment field is 65535 bytes because the field is an unsigned 16-bit number. + + long fileSize = zip.size(); + if (fileSize < ZIP_EOCD_REC_MIN_SIZE) { + return null; + } + + // Optimization: 99.99% of APKs have a zero-length comment field in the EoCD record and thus + // the EoCD record offset is known in advance. Try that offset first to avoid unnecessarily + // reading more data. + Pair<ByteBuffer, Long> result = findZipEndOfCentralDirectoryRecord(zip, 0); + if (result != null) { + return result; + } + + // EoCD does not start where we expected it to. Perhaps it contains a non-empty comment + // field. Expand the search. The maximum size of the comment field in EoCD is 65535 because + // the comment length field is an unsigned 16-bit number. + return findZipEndOfCentralDirectoryRecord(zip, UINT16_MAX_VALUE); + } + + /** + * Returns the ZIP End of Central Directory record of the provided ZIP file. + * + * @param maxCommentSize maximum accepted size (in bytes) of EoCD comment field. The permitted + * value is from 0 to 65535 inclusive. The smaller the value, the faster this method + * locates the record, provided its comment field is no longer than this value. + * + * @return contents of the ZIP End of Central Directory record and the record's offset in the + * file or {@code null} if the file does not contain the record. + * + * @throws IOException if an I/O error occurs while reading the file. + */ + private static Pair<ByteBuffer, Long> findZipEndOfCentralDirectoryRecord( + DataSource zip, int maxCommentSize) throws IOException { + // ZIP End of Central Directory (EOCD) record is located at the very end of the ZIP archive. + // The record can be identified by its 4-byte signature/magic which is located at the very + // beginning of the record. A complication is that the record is variable-length because of + // the comment field. + // The algorithm for locating the ZIP EOCD record is as follows. We search backwards from + // end of the buffer for the EOCD record signature. Whenever we find a signature, we check + // the candidate record's comment length is such that the remainder of the record takes up + // exactly the remaining bytes in the buffer. The search is bounded because the maximum + // size of the comment field is 65535 bytes because the field is an unsigned 16-bit number. + + if ((maxCommentSize < 0) || (maxCommentSize > UINT16_MAX_VALUE)) { + throw new IllegalArgumentException("maxCommentSize: " + maxCommentSize); + } + + long fileSize = zip.size(); + if (fileSize < ZIP_EOCD_REC_MIN_SIZE) { + // No space for EoCD record in the file. + return null; + } + // Lower maxCommentSize if the file is too small. + maxCommentSize = (int) Math.min(maxCommentSize, fileSize - ZIP_EOCD_REC_MIN_SIZE); + + int maxEocdSize = ZIP_EOCD_REC_MIN_SIZE + maxCommentSize; + long bufOffsetInFile = fileSize - maxEocdSize; + ByteBuffer buf = zip.getByteBuffer(bufOffsetInFile, maxEocdSize); + buf.order(ByteOrder.LITTLE_ENDIAN); + int eocdOffsetInBuf = findZipEndOfCentralDirectoryRecord(buf); + if (eocdOffsetInBuf == -1) { + // No EoCD record found in the buffer + return null; + } + // EoCD found + buf.position(eocdOffsetInBuf); + ByteBuffer eocd = buf.slice(); + eocd.order(ByteOrder.LITTLE_ENDIAN); + return Pair.of(eocd, bufOffsetInFile + eocdOffsetInBuf); + } + + /** + * Returns the position at which ZIP End of Central Directory record starts in the provided + * buffer or {@code -1} if the record is not present. + * + * <p>NOTE: Byte order of {@code zipContents} must be little-endian. + */ + private static int findZipEndOfCentralDirectoryRecord(ByteBuffer zipContents) { + assertByteOrderLittleEndian(zipContents); + + // ZIP End of Central Directory (EOCD) record is located at the very end of the ZIP archive. + // The record can be identified by its 4-byte signature/magic which is located at the very + // beginning of the record. A complication is that the record is variable-length because of + // the comment field. + // The algorithm for locating the ZIP EOCD record is as follows. We search backwards from + // end of the buffer for the EOCD record signature. Whenever we find a signature, we check + // the candidate record's comment length is such that the remainder of the record takes up + // exactly the remaining bytes in the buffer. The search is bounded because the maximum + // size of the comment field is 65535 bytes because the field is an unsigned 16-bit number. + + int archiveSize = zipContents.capacity(); + if (archiveSize < ZIP_EOCD_REC_MIN_SIZE) { + return -1; + } + int maxCommentLength = Math.min(archiveSize - ZIP_EOCD_REC_MIN_SIZE, UINT16_MAX_VALUE); + int eocdWithEmptyCommentStartPosition = archiveSize - ZIP_EOCD_REC_MIN_SIZE; + for (int expectedCommentLength = 0; expectedCommentLength <= maxCommentLength; + expectedCommentLength++) { + int eocdStartPos = eocdWithEmptyCommentStartPosition - expectedCommentLength; + if (zipContents.getInt(eocdStartPos) == ZIP_EOCD_REC_SIG) { + int actualCommentLength = + getUnsignedInt16( + zipContents, eocdStartPos + ZIP_EOCD_COMMENT_LENGTH_FIELD_OFFSET); + if (actualCommentLength == expectedCommentLength) { + return eocdStartPos; + } + } + } + + return -1; + } + + static void assertByteOrderLittleEndian(ByteBuffer buffer) { + if (buffer.order() != ByteOrder.LITTLE_ENDIAN) { + throw new IllegalArgumentException("ByteBuffer byte order must be little endian"); + } + } + + public static int getUnsignedInt16(ByteBuffer buffer, int offset) { + return buffer.getShort(offset) & 0xffff; + } + + public static int getUnsignedInt16(ByteBuffer buffer) { + return buffer.getShort() & 0xffff; + } + + public static List<CentralDirectoryRecord> parseZipCentralDirectory( + DataSource apk, + ZipSections apkSections) + throws IOException, ApkFormatException { + // Read the ZIP Central Directory + long cdSizeBytes = apkSections.getZipCentralDirectorySizeBytes(); + if (cdSizeBytes > Integer.MAX_VALUE) { + throw new ApkFormatException("ZIP Central Directory too large: " + cdSizeBytes); + } + long cdOffset = apkSections.getZipCentralDirectoryOffset(); + ByteBuffer cd = apk.getByteBuffer(cdOffset, (int) cdSizeBytes); + cd.order(ByteOrder.LITTLE_ENDIAN); + + // Parse the ZIP Central Directory + int expectedCdRecordCount = apkSections.getZipCentralDirectoryRecordCount(); + List<CentralDirectoryRecord> cdRecords = new ArrayList<>(expectedCdRecordCount); + for (int i = 0; i < expectedCdRecordCount; i++) { + CentralDirectoryRecord cdRecord; + int offsetInsideCd = cd.position(); + try { + cdRecord = CentralDirectoryRecord.getRecord(cd); + } catch (ZipFormatException e) { + throw new ApkFormatException( + "Malformed ZIP Central Directory record #" + (i + 1) + + " at file offset " + (cdOffset + offsetInsideCd), + e); + } + String entryName = cdRecord.getName(); + if (entryName.endsWith("/")) { + // Ignore directory entries + continue; + } + cdRecords.add(cdRecord); + } + // There may be more data in Central Directory, but we don't warn or throw because Android + // ignores unused CD data. + + return cdRecords; + } + + static void setUnsignedInt16(ByteBuffer buffer, int offset, int value) { + if ((value < 0) || (value > 0xffff)) { + throw new IllegalArgumentException("uint16 value of out range: " + value); + } + buffer.putShort(offset, (short) value); + } + + static void setUnsignedInt32(ByteBuffer buffer, int offset, long value) { + if ((value < 0) || (value > 0xffffffffL)) { + throw new IllegalArgumentException("uint32 value of out range: " + value); + } + buffer.putInt(offset, (int) value); + } + + public static void putUnsignedInt16(ByteBuffer buffer, int value) { + if ((value < 0) || (value > 0xffff)) { + throw new IllegalArgumentException("uint16 value of out range: " + value); + } + buffer.putShort((short) value); + } + + static long getUnsignedInt32(ByteBuffer buffer, int offset) { + return buffer.getInt(offset) & 0xffffffffL; + } + + static long getUnsignedInt32(ByteBuffer buffer) { + return buffer.getInt() & 0xffffffffL; + } + + static void putUnsignedInt32(ByteBuffer buffer, long value) { + if ((value < 0) || (value > 0xffffffffL)) { + throw new IllegalArgumentException("uint32 value of out range: " + value); + } + buffer.putInt((int) value); + } + + public static DeflateResult deflate(ByteBuffer input) { + byte[] inputBuf; + int inputOffset; + int inputLength = input.remaining(); + if (input.hasArray()) { + inputBuf = input.array(); + inputOffset = input.arrayOffset() + input.position(); + input.position(input.limit()); + } else { + inputBuf = new byte[inputLength]; + inputOffset = 0; + input.get(inputBuf); + } + CRC32 crc32 = new CRC32(); + crc32.update(inputBuf, inputOffset, inputLength); + long crc32Value = crc32.getValue(); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Deflater deflater = new Deflater(9, true); + deflater.setInput(inputBuf, inputOffset, inputLength); + deflater.finish(); + byte[] buf = new byte[65536]; + while (!deflater.finished()) { + int chunkSize = deflater.deflate(buf); + out.write(buf, 0, chunkSize); + } + return new DeflateResult(inputLength, crc32Value, out.toByteArray()); + } + + public static class DeflateResult { + public final int inputSizeBytes; + public final long inputCrc32; + public final byte[] output; + + public DeflateResult(int inputSizeBytes, long inputCrc32, byte[] output) { + this.inputSizeBytes = inputSizeBytes; + this.inputCrc32 = inputCrc32; + this.output = output; + } + } +}
\ No newline at end of file diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/util/DataSink.java b/platform/android/java/editor/src/main/java/com/android/apksig/util/DataSink.java new file mode 100644 index 0000000000..5042933f1f --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/util/DataSink.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.util; + +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * Consumer of input data which may be provided in one go or in chunks. + */ +public interface DataSink { + + /** + * Consumes the provided chunk of data. + * + * <p>This data sink guarantees to not hold references to the provided buffer after this method + * terminates. + * + * @throws IndexOutOfBoundsException if {@code offset} or {@code length} are negative, or if + * {@code offset + length} is greater than {@code buf.length}. + */ + void consume(byte[] buf, int offset, int length) throws IOException; + + /** + * Consumes all remaining data in the provided buffer and advances the buffer's position + * to the buffer's limit. + * + * <p>This data sink guarantees to not hold references to the provided buffer after this method + * terminates. + */ + void consume(ByteBuffer buf) throws IOException; +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/util/DataSinks.java b/platform/android/java/editor/src/main/java/com/android/apksig/util/DataSinks.java new file mode 100644 index 0000000000..d9562d8341 --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/util/DataSinks.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.util; + +import com.android.apksig.internal.util.ByteArrayDataSink; +import com.android.apksig.internal.util.MessageDigestSink; +import com.android.apksig.internal.util.OutputStreamDataSink; +import com.android.apksig.internal.util.RandomAccessFileDataSink; +import java.io.OutputStream; +import java.io.RandomAccessFile; +import java.security.MessageDigest; + +/** + * Utility methods for working with {@link DataSink} abstraction. + */ +public abstract class DataSinks { + private DataSinks() {} + + /** + * Returns a {@link DataSink} which outputs received data into the provided + * {@link OutputStream}. + */ + public static DataSink asDataSink(OutputStream out) { + return new OutputStreamDataSink(out); + } + + /** + * Returns a {@link DataSink} which outputs received data into the provided file, sequentially, + * starting at the beginning of the file. + */ + public static DataSink asDataSink(RandomAccessFile file) { + return new RandomAccessFileDataSink(file); + } + + /** + * Returns a {@link DataSink} which forwards data into the provided {@link MessageDigest} + * instances via their {@code update} method. Each {@code MessageDigest} instance receives the + * same data. + */ + public static DataSink asDataSink(MessageDigest... digests) { + return new MessageDigestSink(digests); + } + + /** + * Returns a new in-memory {@link DataSink} which exposes all data consumed so far via the + * {@link DataSource} interface. + */ + public static ReadableDataSink newInMemoryDataSink() { + return new ByteArrayDataSink(); + } + + /** + * Returns a new in-memory {@link DataSink} which exposes all data consumed so far via the + * {@link DataSource} interface. + * + * @param initialCapacity initial capacity in bytes + */ + public static ReadableDataSink newInMemoryDataSink(int initialCapacity) { + return new ByteArrayDataSink(initialCapacity); + } +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/util/DataSource.java b/platform/android/java/editor/src/main/java/com/android/apksig/util/DataSource.java new file mode 100644 index 0000000000..a89a87c5f8 --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/util/DataSource.java @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.util; + +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * Abstract representation of a source of data. + * + * <p>This abstraction serves three purposes: + * <ul> + * <li>Transparent handling of different types of sources, such as {@code byte[]}, + * {@link java.nio.ByteBuffer}, {@link java.io.RandomAccessFile}, memory-mapped file.</li> + * <li>Support sources larger than 2 GB. If all sources were smaller than 2 GB, {@code ByteBuffer} + * may have worked as the unifying abstraction.</li> + * <li>Support sources which do not fit into logical memory as a contiguous region.</li> + * </ul> + * + * <p>There are following ways to obtain a chunk of data from the data source: + * <ul> + * <li>Stream the chunk's data into a {@link DataSink} using + * {@link #feed(long, long, DataSink) feed}. This is best suited for scenarios where there is no + * need to have the chunk's data accessible at the same time, for example, when computing the + * digest of the chunk. If you need to keep the chunk's data around after {@code feed} + * completes, you must create a copy during {@code feed}. However, in that case the following + * methods of obtaining the chunk's data may be more appropriate.</li> + * <li>Obtain a {@link ByteBuffer} containing the chunk's data using + * {@link #getByteBuffer(long, int) getByteBuffer}. Depending on the data source, the chunk's + * data may or may not be copied by this operation. This is best suited for scenarios where + * you need to access the chunk's data in arbitrary order, but don't need to modify the data and + * thus don't require a copy of the data.</li> + * <li>Copy the chunk's data to a {@link ByteBuffer} using + * {@link #copyTo(long, int, ByteBuffer) copyTo}. This is best suited for scenarios where + * you require a copy of the chunk's data, such as to when you need to modify the data. + * </li> + * </ul> + */ +public interface DataSource { + + /** + * Returns the amount of data (in bytes) contained in this data source. + */ + long size(); + + /** + * Feeds the specified chunk from this data source into the provided sink. + * + * @param offset index (in bytes) at which the chunk starts inside data source + * @param size size (in bytes) of the chunk + * + * @throws IndexOutOfBoundsException if {@code offset} or {@code size} is negative, or if + * {@code offset + size} is greater than {@link #size()}. + */ + void feed(long offset, long size, DataSink sink) throws IOException; + + /** + * Returns a buffer holding the contents of the specified chunk of data from this data source. + * Changes to the data source are not guaranteed to be reflected in the returned buffer. + * Similarly, changes in the buffer are not guaranteed to be reflected in the data source. + * + * <p>The returned buffer's position is {@code 0}, and the buffer's limit and capacity is + * {@code size}. + * + * @param offset index (in bytes) at which the chunk starts inside data source + * @param size size (in bytes) of the chunk + * + * @throws IndexOutOfBoundsException if {@code offset} or {@code size} is negative, or if + * {@code offset + size} is greater than {@link #size()}. + */ + ByteBuffer getByteBuffer(long offset, int size) throws IOException; + + /** + * Copies the specified chunk from this data source into the provided destination buffer, + * advancing the destination buffer's position by {@code size}. + * + * @param offset index (in bytes) at which the chunk starts inside data source + * @param size size (in bytes) of the chunk + * + * @throws IndexOutOfBoundsException if {@code offset} or {@code size} is negative, or if + * {@code offset + size} is greater than {@link #size()}. + */ + void copyTo(long offset, int size, ByteBuffer dest) throws IOException; + + /** + * Returns a data source representing the specified region of data of this data source. Changes + * to data represented by this data source will also be visible in the returned data source. + * + * @param offset index (in bytes) at which the region starts inside data source + * @param size size (in bytes) of the region + * + * @throws IndexOutOfBoundsException if {@code offset} or {@code size} is negative, or if + * {@code offset + size} is greater than {@link #size()}. + */ + DataSource slice(long offset, long size); +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/util/DataSources.java b/platform/android/java/editor/src/main/java/com/android/apksig/util/DataSources.java new file mode 100644 index 0000000000..1f0b40b66a --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/util/DataSources.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.util; + +import com.android.apksig.internal.util.ByteBufferDataSource; +import com.android.apksig.internal.util.FileChannelDataSource; +import java.io.RandomAccessFile; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; + +/** + * Utility methods for working with {@link DataSource} abstraction. + */ +public abstract class DataSources { + private DataSources() {} + + /** + * Returns a {@link DataSource} backed by the provided {@link ByteBuffer}. The data source + * represents the data contained between the position and limit of the buffer. Changes to the + * buffer's contents will be visible in the data source. + */ + public static DataSource asDataSource(ByteBuffer buffer) { + if (buffer == null) { + throw new NullPointerException(); + } + return new ByteBufferDataSource(buffer); + } + + /** + * Returns a {@link DataSource} backed by the provided {@link RandomAccessFile}. Changes to the + * file, including changes to size of file, will be visible in the data source. + */ + public static DataSource asDataSource(RandomAccessFile file) { + return asDataSource(file.getChannel()); + } + + /** + * Returns a {@link DataSource} backed by the provided region of the {@link RandomAccessFile}. + * Changes to the file will be visible in the data source. + */ + public static DataSource asDataSource(RandomAccessFile file, long offset, long size) { + return asDataSource(file.getChannel(), offset, size); + } + + /** + * Returns a {@link DataSource} backed by the provided {@link FileChannel}. Changes to the + * file, including changes to size of file, will be visible in the data source. + */ + public static DataSource asDataSource(FileChannel channel) { + if (channel == null) { + throw new NullPointerException(); + } + return new FileChannelDataSource(channel); + } + + /** + * Returns a {@link DataSource} backed by the provided region of the {@link FileChannel}. + * Changes to the file will be visible in the data source. + */ + public static DataSource asDataSource(FileChannel channel, long offset, long size) { + if (channel == null) { + throw new NullPointerException(); + } + return new FileChannelDataSource(channel, offset, size); + } +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/util/ReadableDataSink.java b/platform/android/java/editor/src/main/java/com/android/apksig/util/ReadableDataSink.java new file mode 100644 index 0000000000..ffc3e2d351 --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/util/ReadableDataSink.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.util; + +/** + * {@link DataSink} which exposes all data consumed so far as a {@link DataSource}. This abstraction + * offers append-only write access and random read access. + */ +public interface ReadableDataSink extends DataSink, DataSource { +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/util/RunnablesExecutor.java b/platform/android/java/editor/src/main/java/com/android/apksig/util/RunnablesExecutor.java new file mode 100644 index 0000000000..74017f8d8c --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/util/RunnablesExecutor.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.util; + +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Phaser; +import java.util.concurrent.ThreadPoolExecutor; + +public interface RunnablesExecutor { + static final RunnablesExecutor SINGLE_THREADED = p -> p.createRunnable().run(); + + static final RunnablesExecutor MULTI_THREADED = new RunnablesExecutor() { + private final int PARALLELISM = Math.min(32, Runtime.getRuntime().availableProcessors()); + private final int QUEUE_SIZE = 4; + + @Override + public void execute(RunnablesProvider provider) { + final ExecutorService mExecutor = + new ThreadPoolExecutor(PARALLELISM, PARALLELISM, + 0L, MILLISECONDS, + new ArrayBlockingQueue<>(QUEUE_SIZE), + new ThreadPoolExecutor.CallerRunsPolicy()); + + Phaser tasks = new Phaser(1); + + for (int i = 0; i < PARALLELISM; ++i) { + Runnable task = () -> { + Runnable r = provider.createRunnable(); + r.run(); + tasks.arriveAndDeregister(); + }; + tasks.register(); + mExecutor.execute(task); + } + + // Waiting for the tasks to complete. + tasks.arriveAndAwaitAdvance(); + + mExecutor.shutdownNow(); + } + }; + + void execute(RunnablesProvider provider); +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/util/RunnablesProvider.java b/platform/android/java/editor/src/main/java/com/android/apksig/util/RunnablesProvider.java new file mode 100644 index 0000000000..f96dcfe4d1 --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/util/RunnablesProvider.java @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.util; + +public interface RunnablesProvider { + Runnable createRunnable(); +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/zip/ZipFormatException.java b/platform/android/java/editor/src/main/java/com/android/apksig/zip/ZipFormatException.java new file mode 100644 index 0000000000..6116c0da80 --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/zip/ZipFormatException.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.zip; + +/** + * Indicates that a ZIP archive is not well-formed. + */ +public class ZipFormatException extends Exception { + private static final long serialVersionUID = 1L; + + public ZipFormatException(String message) { + super(message); + } + + public ZipFormatException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/platform/android/java/editor/src/main/java/com/android/apksig/zip/ZipSections.java b/platform/android/java/editor/src/main/java/com/android/apksig/zip/ZipSections.java new file mode 100644 index 0000000000..17bce05187 --- /dev/null +++ b/platform/android/java/editor/src/main/java/com/android/apksig/zip/ZipSections.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.zip; + +import java.nio.ByteBuffer; + +/** + * Base representation of an APK's zip sections containing the central directory's offset, the size + * of the central directory in bytes, the number of records in the central directory, the offset + * of the end of central directory, and a ByteBuffer containing the end of central directory + * contents. + */ +public class ZipSections { + private final long mCentralDirectoryOffset; + private final long mCentralDirectorySizeBytes; + private final int mCentralDirectoryRecordCount; + private final long mEocdOffset; + private final ByteBuffer mEocd; + + public ZipSections( + long centralDirectoryOffset, + long centralDirectorySizeBytes, + int centralDirectoryRecordCount, + long eocdOffset, + ByteBuffer eocd) { + mCentralDirectoryOffset = centralDirectoryOffset; + mCentralDirectorySizeBytes = centralDirectorySizeBytes; + mCentralDirectoryRecordCount = centralDirectoryRecordCount; + mEocdOffset = eocdOffset; + mEocd = eocd; + } + + /** + * Returns the start offset of the ZIP Central Directory. This value is taken from the + * ZIP End of Central Directory record. + */ + public long getZipCentralDirectoryOffset() { + return mCentralDirectoryOffset; + } + + /** + * Returns the size (in bytes) of the ZIP Central Directory. This value is taken from the + * ZIP End of Central Directory record. + */ + public long getZipCentralDirectorySizeBytes() { + return mCentralDirectorySizeBytes; + } + + /** + * Returns the number of records in the ZIP Central Directory. This value is taken from the + * ZIP End of Central Directory record. + */ + public int getZipCentralDirectoryRecordCount() { + return mCentralDirectoryRecordCount; + } + + /** + * Returns the start offset of the ZIP End of Central Directory record. The record extends + * until the very end of the APK. + */ + public long getZipEndOfCentralDirectoryOffset() { + return mEocdOffset; + } + + /** + * Returns the contents of the ZIP End of Central Directory. + */ + public ByteBuffer getZipEndOfCentralDirectory() { + return mEocd; + } +}
\ No newline at end of file diff --git a/platform/android/java/editor/src/main/java/org/godotengine/editor/EditorMessageDispatcher.kt b/platform/android/java/editor/src/main/java/org/godotengine/editor/EditorMessageDispatcher.kt new file mode 100644 index 0000000000..b16e62149a --- /dev/null +++ b/platform/android/java/editor/src/main/java/org/godotengine/editor/EditorMessageDispatcher.kt @@ -0,0 +1,192 @@ +/**************************************************************************/ +/* EditorMessageDispatcher.kt */ +/**************************************************************************/ +/* 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. */ +/**************************************************************************/ + +package org.godotengine.editor + +import android.annotation.SuppressLint +import android.content.Intent +import android.content.pm.PackageManager +import android.os.Bundle +import android.os.Handler +import android.os.Message +import android.os.Messenger +import android.os.RemoteException +import android.util.Log +import java.util.concurrent.ConcurrentHashMap + +/** + * Used by the [GodotEditor] classes to dispatch messages across processes. + */ +internal class EditorMessageDispatcher(private val editor: GodotEditor) { + + companion object { + private val TAG = EditorMessageDispatcher::class.java.simpleName + + /** + * Extra used to pass the message dispatcher payload through an [Intent] + */ + const val EXTRA_MSG_DISPATCHER_PAYLOAD = "message_dispatcher_payload" + + /** + * Key used to pass the editor id through a [Bundle] + */ + private const val KEY_EDITOR_ID = "editor_id" + + /** + * Key used to pass the editor messenger through a [Bundle] + */ + private const val KEY_EDITOR_MESSENGER = "editor_messenger" + + /** + * Requests the recipient to quit right away. + */ + private const val MSG_FORCE_QUIT = 0 + + /** + * Requests the recipient to store the passed [android.os.Messenger] instance. + */ + private const val MSG_REGISTER_MESSENGER = 1 + } + + private val recipientsMessengers = ConcurrentHashMap<Int, Messenger>() + + @SuppressLint("HandlerLeak") + private val dispatcherHandler = object : Handler() { + override fun handleMessage(msg: Message) { + when (msg.what) { + MSG_FORCE_QUIT -> editor.finish() + + MSG_REGISTER_MESSENGER -> { + val editorId = msg.arg1 + val messenger = msg.replyTo + registerMessenger(editorId, messenger) + } + + else -> super.handleMessage(msg) + } + } + } + + /** + * Request the window with the given [editorId] to force quit. + */ + fun requestForceQuit(editorId: Int): Boolean { + val messenger = recipientsMessengers[editorId] ?: return false + return try { + Log.v(TAG, "Requesting 'forceQuit' for $editorId") + val msg = Message.obtain(null, MSG_FORCE_QUIT) + messenger.send(msg) + true + } catch (e: RemoteException) { + Log.e(TAG, "Error requesting 'forceQuit' to $editorId", e) + recipientsMessengers.remove(editorId) + false + } + } + + /** + * Utility method to register a receiver messenger. + */ + private fun registerMessenger(editorId: Int, messenger: Messenger?, messengerDeathCallback: Runnable? = null) { + try { + if (messenger == null) { + Log.w(TAG, "Invalid 'replyTo' payload") + } else if (messenger.binder.isBinderAlive) { + messenger.binder.linkToDeath({ + Log.v(TAG, "Removing messenger for $editorId") + recipientsMessengers.remove(editorId) + messengerDeathCallback?.run() + }, 0) + recipientsMessengers[editorId] = messenger + } + } catch (e: RemoteException) { + Log.e(TAG, "Unable to register messenger from $editorId", e) + recipientsMessengers.remove(editorId) + } + } + + /** + * Utility method to register a [Messenger] attached to this handler with a host. + * + * This is done so that the host can send request to the editor instance attached to this handle. + * + * Note that this is only done when the editor instance is internal (not exported) to prevent + * arbitrary apps from having the ability to send requests. + */ + private fun registerSelfTo(pm: PackageManager, host: Messenger?, selfId: Int) { + try { + if (host == null || !host.binder.isBinderAlive) { + Log.v(TAG, "Host is unavailable") + return + } + + val activityInfo = pm.getActivityInfo(editor.componentName, 0) + if (activityInfo.exported) { + Log.v(TAG, "Not registering self to host as we're exported") + return + } + + Log.v(TAG, "Registering self $selfId to host") + val msg = Message.obtain(null, MSG_REGISTER_MESSENGER) + msg.arg1 = selfId + msg.replyTo = Messenger(dispatcherHandler) + host.send(msg) + } catch (e: RemoteException) { + Log.e(TAG, "Unable to register self with host", e) + } + } + + /** + * Parses the starting intent and retrieve an editor messenger if available + */ + fun parseStartIntent(pm: PackageManager, intent: Intent) { + val messengerBundle = intent.getBundleExtra(EXTRA_MSG_DISPATCHER_PAYLOAD) ?: return + + // Retrieve the sender messenger payload and store it. This can be used to communicate back + // to the sender. + val senderId = messengerBundle.getInt(KEY_EDITOR_ID) + val senderMessenger: Messenger? = messengerBundle.getParcelable(KEY_EDITOR_MESSENGER) + registerMessenger(senderId, senderMessenger) + + // Register ourselves to the sender so that it can communicate with us. + registerSelfTo(pm, senderMessenger, editor.getEditorWindowInfo().windowId) + } + + /** + * Returns the payload used by the [EditorMessageDispatcher] class to establish an IPC bridge + * across editor instances. + */ + fun getMessageDispatcherPayload(): Bundle { + return Bundle().apply { + putInt(KEY_EDITOR_ID, editor.getEditorWindowInfo().windowId) + putParcelable(KEY_EDITOR_MESSENGER, Messenger(dispatcherHandler)) + } + } +} diff --git a/platform/android/java/editor/src/main/java/org/godotengine/editor/EditorWindowInfo.kt b/platform/android/java/editor/src/main/java/org/godotengine/editor/EditorWindowInfo.kt index 0da1d01aed..d3daa1dbbc 100644 --- a/platform/android/java/editor/src/main/java/org/godotengine/editor/EditorWindowInfo.kt +++ b/platform/android/java/editor/src/main/java/org/godotengine/editor/EditorWindowInfo.kt @@ -31,23 +31,24 @@ package org.godotengine.editor /** - * Specifies the policy for adjacent launches. + * Specifies the policy for launches. */ -enum class LaunchAdjacentPolicy { +enum class LaunchPolicy { /** - * Adjacent launches are disabled. + * Launch policy is determined by the editor settings or based on the device and screen metrics. */ - DISABLED, + AUTO, + /** - * Adjacent launches are enabled / disabled based on the device and screen metrics. + * Launches happen in the same window. */ - AUTO, + SAME, /** * Adjacent launches are enabled. */ - ENABLED + ADJACENT } /** @@ -57,12 +58,14 @@ data class EditorWindowInfo( val windowClassName: String, val windowId: Int, val processNameSuffix: String, - val launchAdjacentPolicy: LaunchAdjacentPolicy = LaunchAdjacentPolicy.DISABLED + val launchPolicy: LaunchPolicy = LaunchPolicy.SAME, + val supportsPiPMode: Boolean = false ) { constructor( windowClass: Class<*>, windowId: Int, processNameSuffix: String, - launchAdjacentPolicy: LaunchAdjacentPolicy = LaunchAdjacentPolicy.DISABLED - ) : this(windowClass.name, windowId, processNameSuffix, launchAdjacentPolicy) + launchPolicy: LaunchPolicy = LaunchPolicy.SAME, + supportsPiPMode: Boolean = false + ) : this(windowClass.name, windowId, processNameSuffix, launchPolicy, supportsPiPMode) } diff --git a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt index 7c11d69609..1995a38c2a 100644 --- a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt +++ b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt @@ -32,6 +32,7 @@ package org.godotengine.editor import android.Manifest import android.app.ActivityManager +import android.app.ActivityOptions import android.content.ComponentName import android.content.Context import android.content.Intent @@ -39,12 +40,16 @@ import android.content.pm.PackageManager import android.os.* import android.util.Log import android.view.View +import android.view.WindowManager import android.widget.Toast import androidx.annotation.CallSuper import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.window.layout.WindowMetricsCalculator +import org.godotengine.editor.utils.signApk +import org.godotengine.editor.utils.verifyApk import org.godotengine.godot.GodotActivity import org.godotengine.godot.GodotLib +import org.godotengine.godot.error.Error import org.godotengine.godot.utils.PermissionsUtil import org.godotengine.godot.utils.ProcessPhoenix import java.util.* @@ -66,17 +71,26 @@ open class GodotEditor : GodotActivity() { private const val WAIT_FOR_DEBUGGER = false - private const val EXTRA_COMMAND_LINE_PARAMS = "command_line_params" + @JvmStatic + protected val EXTRA_COMMAND_LINE_PARAMS = "command_line_params" + @JvmStatic + protected val EXTRA_PIP_AVAILABLE = "pip_available" + @JvmStatic + protected val EXTRA_LAUNCH_IN_PIP = "launch_in_pip_requested" // Command line arguments + private const val FULLSCREEN_ARG = "--fullscreen" + private const val FULLSCREEN_ARG_SHORT = "-f" private const val EDITOR_ARG = "--editor" private const val EDITOR_ARG_SHORT = "-e" private const val EDITOR_PROJECT_MANAGER_ARG = "--project-manager" private const val EDITOR_PROJECT_MANAGER_ARG_SHORT = "-p" + private const val BREAKPOINTS_ARG = "--breakpoints" + private const val BREAKPOINTS_ARG_SHORT = "-b" // Info for the various classes used by the editor internal val EDITOR_MAIN_INFO = EditorWindowInfo(GodotEditor::class.java, 777, "") - internal val RUN_GAME_INFO = EditorWindowInfo(GodotGame::class.java, 667, ":GodotGame", LaunchAdjacentPolicy.AUTO) + internal val RUN_GAME_INFO = EditorWindowInfo(GodotGame::class.java, 667, ":GodotGame", LaunchPolicy.AUTO, true) /** * Sets of constants to specify the window to use to run the project. @@ -87,16 +101,34 @@ open class GodotEditor : GodotActivity() { private const val ANDROID_WINDOW_AUTO = 0 private const val ANDROID_WINDOW_SAME_AS_EDITOR = 1 private const val ANDROID_WINDOW_SIDE_BY_SIDE_WITH_EDITOR = 2 + + /** + * Sets of constants to specify the Play window PiP mode. + * + * Should match the values in `editor/editor_settings.cpp'` for the + * 'run/window_placement/play_window_pip_mode' setting. + */ + private const val PLAY_WINDOW_PIP_DISABLED = 0 + private const val PLAY_WINDOW_PIP_ENABLED = 1 + private const val PLAY_WINDOW_PIP_ENABLED_FOR_SAME_AS_EDITOR = 2 } + private val editorMessageDispatcher = EditorMessageDispatcher(this) private val commandLineParams = ArrayList<String>() private val editorLoadingIndicator: View? by lazy { findViewById(R.id.editor_loading_indicator) } override fun getGodotAppLayout() = R.layout.godot_editor_layout + internal open fun getEditorWindowInfo() = EDITOR_MAIN_INFO + override fun onCreate(savedInstanceState: Bundle?) { installSplashScreen() + // Prevent the editor window from showing in the display cutout + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && getEditorWindowInfo() == EDITOR_MAIN_INFO) { + window.attributes.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER + } + // We exclude certain permissions from the set we request at startup, as they'll be // requested on demand based on use-cases. PermissionsUtil.requestManifestPermissions(this, setOf(Manifest.permission.RECORD_AUDIO)) @@ -105,6 +137,8 @@ open class GodotEditor : GodotActivity() { Log.d(TAG, "Starting intent $intent with parameters ${params.contentToString()}") updateCommandLineParams(params?.asList() ?: emptyList()) + editorMessageDispatcher.parseStartIntent(packageManager, intent) + if (BuildConfig.BUILD_TYPE == "dev" && WAIT_FOR_DEBUGGER) { Debug.waitForDebugger() } @@ -186,35 +220,81 @@ open class GodotEditor : GodotActivity() { } } - override fun onNewGodotInstanceRequested(args: Array<String>): Int { - val editorWindowInfo = getEditorWindowInfo(args) + protected fun getNewGodotInstanceIntent(editorWindowInfo: EditorWindowInfo, args: Array<String>): Intent { + val updatedArgs = if (editorWindowInfo == EDITOR_MAIN_INFO && + godot?.isInImmersiveMode() == true && + !args.contains(FULLSCREEN_ARG) && + !args.contains(FULLSCREEN_ARG_SHORT) + ) { + // If we're launching an editor window (project manager or editor) and we're in + // fullscreen mode, we want to remain in fullscreen mode. + // This doesn't apply to the play / game window since for that window fullscreen is + // controlled by the game logic. + args + FULLSCREEN_ARG + } else { + args + } - // Launch a new activity val newInstance = Intent() .setComponent(ComponentName(this, editorWindowInfo.windowClassName)) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - .putExtra(EXTRA_COMMAND_LINE_PARAMS, args) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - if (editorWindowInfo.launchAdjacentPolicy == LaunchAdjacentPolicy.ENABLED || - (editorWindowInfo.launchAdjacentPolicy == LaunchAdjacentPolicy.AUTO && shouldGameLaunchAdjacent())) { + .putExtra(EXTRA_COMMAND_LINE_PARAMS, updatedArgs) + + val launchPolicy = resolveLaunchPolicyIfNeeded(editorWindowInfo.launchPolicy) + val isPiPAvailable = if (editorWindowInfo.supportsPiPMode && hasPiPSystemFeature()) { + val pipMode = getPlayWindowPiPMode() + pipMode == PLAY_WINDOW_PIP_ENABLED || + (pipMode == PLAY_WINDOW_PIP_ENABLED_FOR_SAME_AS_EDITOR && launchPolicy == LaunchPolicy.SAME) + } else { + false + } + newInstance.putExtra(EXTRA_PIP_AVAILABLE, isPiPAvailable) + + if (launchPolicy == LaunchPolicy.ADJACENT) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { Log.v(TAG, "Adding flag for adjacent launch") newInstance.addFlags(Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT) } + } else if (launchPolicy == LaunchPolicy.SAME) { + if (isPiPAvailable && + (updatedArgs.contains(BREAKPOINTS_ARG) || updatedArgs.contains(BREAKPOINTS_ARG_SHORT))) { + Log.v(TAG, "Launching in PiP mode because of breakpoints") + newInstance.putExtra(EXTRA_LAUNCH_IN_PIP, true) + } + } + + return newInstance + } + + override fun onNewGodotInstanceRequested(args: Array<String>): Int { + val editorWindowInfo = getEditorWindowInfo(args) + + // Launch a new activity + val sourceView = godotFragment?.view + val activityOptions = if (sourceView == null) { + null + } else { + val startX = sourceView.width / 2 + val startY = sourceView.height / 2 + ActivityOptions.makeScaleUpAnimation(sourceView, startX, startY, 0, 0) } + + val newInstance = getNewGodotInstanceIntent(editorWindowInfo, args) if (editorWindowInfo.windowClassName == javaClass.name) { Log.d(TAG, "Restarting ${editorWindowInfo.windowClassName} with parameters ${args.contentToString()}") val godot = godot if (godot != null) { godot.destroyAndKillProcess { - ProcessPhoenix.triggerRebirth(this, newInstance) + ProcessPhoenix.triggerRebirth(this, activityOptions?.toBundle(), newInstance) } } else { - ProcessPhoenix.triggerRebirth(this, newInstance) + ProcessPhoenix.triggerRebirth(this, activityOptions?.toBundle(), newInstance) } } else { Log.d(TAG, "Starting ${editorWindowInfo.windowClassName} with parameters ${args.contentToString()}") newInstance.putExtra(EXTRA_NEW_LAUNCH, true) - startActivity(newInstance) + .putExtra(EditorMessageDispatcher.EXTRA_MSG_DISPATCHER_PAYLOAD, editorMessageDispatcher.getMessageDispatcherPayload()) + startActivity(newInstance, activityOptions?.toBundle()) } return editorWindowInfo.windowId } @@ -228,6 +308,12 @@ open class GodotEditor : GodotActivity() { return true } + // Send an inter-process message to request the target editor window to force quit. + if (editorMessageDispatcher.requestForceQuit(editorWindowInfo.windowId)) { + return true + } + + // Fallback to killing the target process. val processName = packageName + editorWindowInfo.processNameSuffix val activityManager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager val runningProcesses = activityManager.runningAppProcesses @@ -282,29 +368,65 @@ open class GodotEditor : GodotActivity() { java.lang.Boolean.parseBoolean(GodotLib.getEditorSetting("interface/touchscreen/enable_pan_and_scale_gestures")) /** - * Whether we should launch the new godot instance in an adjacent window - * @see https://developer.android.com/reference/android/content/Intent#FLAG_ACTIVITY_LAUNCH_ADJACENT + * Retrieves the play window pip mode editor setting. + */ + private fun getPlayWindowPiPMode(): Int { + return try { + Integer.parseInt(GodotLib.getEditorSetting("run/window_placement/play_window_pip_mode")) + } catch (e: NumberFormatException) { + PLAY_WINDOW_PIP_ENABLED_FOR_SAME_AS_EDITOR + } + } + + /** + * If the launch policy is [LaunchPolicy.AUTO], resolve it into a specific policy based on the + * editor setting or device and screen metrics. + * + * If the launch policy is [LaunchPolicy.PIP] but PIP is not supported, fallback to the default + * launch policy. */ - private fun shouldGameLaunchAdjacent(): Boolean { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - try { - when (Integer.parseInt(GodotLib.getEditorSetting("run/window_placement/android_window"))) { - ANDROID_WINDOW_SAME_AS_EDITOR -> false - ANDROID_WINDOW_SIDE_BY_SIDE_WITH_EDITOR -> true - else -> { - // ANDROID_WINDOW_AUTO - isInMultiWindowMode || isLargeScreen + private fun resolveLaunchPolicyIfNeeded(policy: LaunchPolicy): LaunchPolicy { + val inMultiWindowMode = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + isInMultiWindowMode + } else { + false + } + val defaultLaunchPolicy = if (inMultiWindowMode || isLargeScreen) { + LaunchPolicy.ADJACENT + } else { + LaunchPolicy.SAME + } + + return when (policy) { + LaunchPolicy.AUTO -> { + try { + when (Integer.parseInt(GodotLib.getEditorSetting("run/window_placement/android_window"))) { + ANDROID_WINDOW_SAME_AS_EDITOR -> LaunchPolicy.SAME + ANDROID_WINDOW_SIDE_BY_SIDE_WITH_EDITOR -> LaunchPolicy.ADJACENT + else -> { + // ANDROID_WINDOW_AUTO + defaultLaunchPolicy + } } + } catch (e: NumberFormatException) { + Log.w(TAG, "Error parsing the Android window placement editor setting", e) + // Fall-back to the default launch policy + defaultLaunchPolicy } - } catch (e: NumberFormatException) { - // Fall-back to the 'Auto' behavior - isInMultiWindowMode || isLargeScreen } - } else { - false + + else -> { + policy + } } } + /** + * Returns true the if the device supports picture-in-picture (PiP) + */ + protected open fun hasPiPSystemFeature() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && + packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE) + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) // Check if we got the MANAGE_EXTERNAL_STORAGE permission @@ -350,4 +472,20 @@ open class GodotEditor : GodotActivity() { } } } + + override fun signApk( + inputPath: String, + outputPath: String, + keystorePath: String, + keystoreUser: String, + keystorePassword: String + ): Error { + val godot = godot ?: return Error.ERR_UNCONFIGURED + return signApk(godot.fileAccessHandler, inputPath, outputPath, keystorePath, keystoreUser, keystorePassword) + } + + override fun verifyApk(apkPath: String): Error { + val godot = godot ?: return Error.ERR_UNCONFIGURED + return verifyApk(godot.fileAccessHandler, apkPath) + } } diff --git a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotGame.kt b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotGame.kt index 2bcfba559c..6b4bf255f2 100644 --- a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotGame.kt +++ b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotGame.kt @@ -30,6 +30,14 @@ package org.godotengine.editor +import android.annotation.SuppressLint +import android.app.PictureInPictureParams +import android.content.Intent +import android.graphics.Rect +import android.os.Build +import android.os.Bundle +import android.util.Log +import android.view.View import org.godotengine.godot.GodotLib /** @@ -37,7 +45,90 @@ import org.godotengine.godot.GodotLib */ class GodotGame : GodotEditor() { - override fun getGodotAppLayout() = org.godotengine.godot.R.layout.godot_app_layout + companion object { + private val TAG = GodotGame::class.java.simpleName + } + + private val gameViewSourceRectHint = Rect() + private val pipButton: View? by lazy { + findViewById(R.id.godot_pip_button) + } + + private var pipAvailable = false + + @SuppressLint("ClickableViewAccessibility") + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val gameView = findViewById<View>(R.id.godot_fragment_container) + gameView?.addOnLayoutChangeListener { v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom -> + gameView.getGlobalVisibleRect(gameViewSourceRectHint) + } + } + + pipButton?.setOnClickListener { enterPiPMode() } + + handleStartIntent(intent) + } + + override fun onNewIntent(newIntent: Intent) { + super.onNewIntent(newIntent) + handleStartIntent(newIntent) + } + + private fun handleStartIntent(intent: Intent) { + pipAvailable = intent.getBooleanExtra(EXTRA_PIP_AVAILABLE, pipAvailable) + updatePiPButtonVisibility() + + val pipLaunchRequested = intent.getBooleanExtra(EXTRA_LAUNCH_IN_PIP, false) + if (pipLaunchRequested) { + enterPiPMode() + } + } + + private fun updatePiPButtonVisibility() { + pipButton?.visibility = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && pipAvailable && !isInPictureInPictureMode) { + View.VISIBLE + } else { + View.GONE + } + } + + private fun enterPiPMode() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && pipAvailable) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val builder = PictureInPictureParams.Builder().setSourceRectHint(gameViewSourceRectHint) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + builder.setSeamlessResizeEnabled(false) + } + setPictureInPictureParams(builder.build()) + } + + Log.v(TAG, "Entering PiP mode") + enterPictureInPictureMode() + } + } + + override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean) { + super.onPictureInPictureModeChanged(isInPictureInPictureMode) + Log.v(TAG, "onPictureInPictureModeChanged: $isInPictureInPictureMode") + updatePiPButtonVisibility() + } + + override fun onStop() { + super.onStop() + + val isInPiPMode = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && isInPictureInPictureMode + if (isInPiPMode && !isFinishing) { + // We get in this state when PiP is closed, so we terminate the activity. + finish() + } + } + + override fun getGodotAppLayout() = R.layout.godot_game_layout + + override fun getEditorWindowInfo() = RUN_GAME_INFO override fun overrideOrientationRequest() = false diff --git a/platform/android/java/editor/src/main/java/org/godotengine/editor/utils/ApkSignerUtil.kt b/platform/android/java/editor/src/main/java/org/godotengine/editor/utils/ApkSignerUtil.kt new file mode 100644 index 0000000000..42c18c9562 --- /dev/null +++ b/platform/android/java/editor/src/main/java/org/godotengine/editor/utils/ApkSignerUtil.kt @@ -0,0 +1,204 @@ +/**************************************************************************/ +/* ApkSignerUtil.kt */ +/**************************************************************************/ +/* 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. */ +/**************************************************************************/ + +@file:JvmName("ApkSignerUtil") + +package org.godotengine.editor.utils + +import android.util.Log +import com.android.apksig.ApkSigner +import com.android.apksig.ApkVerifier +import org.bouncycastle.jce.provider.BouncyCastleProvider +import org.godotengine.godot.error.Error +import org.godotengine.godot.io.file.FileAccessHandler +import java.io.File +import java.security.KeyStore +import java.security.PrivateKey +import java.security.Security +import java.security.cert.X509Certificate +import java.util.ArrayList + + +/** + * Contains utilities methods to sign and verify Android apks using apksigner + */ +private const val TAG = "ApkSignerUtil" + +private const val DEFAULT_KEYSTORE_TYPE = "PKCS12" + +/** + * Validates that the correct version of the BouncyCastleProvider is added. + */ +private fun validateBouncyCastleProvider() { + val bcProvider = Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) + if (bcProvider !is BouncyCastleProvider) { + Log.v(TAG, "Removing BouncyCastleProvider $bcProvider (${bcProvider::class.java.name})") + Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME) + + val updatedBcProvider = BouncyCastleProvider() + val addResult = Security.addProvider(updatedBcProvider) + if (addResult == -1) { + Log.e(TAG, "Unable to add BouncyCastleProvider ${updatedBcProvider::class.java.name}") + } else { + Log.v(TAG, "Updated BouncyCastleProvider to $updatedBcProvider (${updatedBcProvider::class.java.name})") + } + } +} + +/** + * Verifies the given Android apk + * + * @return true if verification was successful, false otherwise. + */ +internal fun verifyApk(fileAccessHandler: FileAccessHandler, apkPath: String): Error { + if (!fileAccessHandler.fileExists(apkPath)) { + Log.e(TAG, "Unable to access apk $apkPath") + return Error.ERR_FILE_NOT_FOUND + } + + try { + val apkVerifier = ApkVerifier.Builder(File(apkPath)).build() + + Log.v(TAG, "Verifying apk $apkPath") + val result = apkVerifier.verify() + + Log.v(TAG, "Verification result: ${result.isVerified}") + return if (result.isVerified) { + Error.OK + } else { + Error.FAILED + } + } catch (e: Exception) { + Log.e(TAG, "Error occurred during verification for $apkPath", e) + return Error.ERR_INVALID_DATA + } +} + +/** + * Signs the given Android apk + * + * @return true if signing is successful, false otherwise. + */ +internal fun signApk(fileAccessHandler: FileAccessHandler, + inputPath: String, + outputPath: String, + keystorePath: String, + keystoreUser: String, + keystorePassword: String, + keystoreType: String = DEFAULT_KEYSTORE_TYPE): Error { + if (!fileAccessHandler.fileExists(inputPath)) { + Log.e(TAG, "Unable to access input path $inputPath") + return Error.ERR_FILE_NOT_FOUND + } + + val tmpOutputPath = if (outputPath != inputPath) { outputPath } else { "$outputPath.signed" } + if (!fileAccessHandler.canAccess(tmpOutputPath)) { + Log.e(TAG, "Unable to access output path $tmpOutputPath") + return Error.ERR_FILE_NO_PERMISSION + } + + if (!fileAccessHandler.fileExists(keystorePath) || + keystoreUser.isBlank() || + keystorePassword.isBlank()) { + Log.e(TAG, "Invalid keystore credentials") + return Error.ERR_INVALID_PARAMETER + } + + validateBouncyCastleProvider() + + // 1. Obtain a KeyStore implementation + val keyStore = KeyStore.getInstance(keystoreType) + + // 2. Load the keystore + val inputStream = fileAccessHandler.getInputStream(keystorePath) + if (inputStream == null) { + Log.e(TAG, "Unable to retrieve input stream from $keystorePath") + return Error.ERR_FILE_CANT_READ + } + try { + inputStream.use { + Log.v(TAG, "Loading keystore $keystorePath with type $keystoreType") + keyStore.load(it, keystorePassword.toCharArray()) + } + } catch (e: Exception) { + Log.e(TAG, "Unable to load the keystore from $keystorePath", e) + return Error.ERR_FILE_CANT_READ + } + + // 3. Load the private key and cert chain from the keystore + if (!keyStore.isKeyEntry(keystoreUser)) { + Log.e(TAG, "Key alias $keystoreUser is invalid") + return Error.ERR_INVALID_PARAMETER + } + + val keyStoreKey = try { + keyStore.getKey(keystoreUser, keystorePassword.toCharArray()) + } catch (e: Exception) { + Log.e(TAG, "Unable to recover keystore alias $keystoreUser") + return Error.ERR_CANT_ACQUIRE_RESOURCE + } + + if (keyStoreKey !is PrivateKey) { + Log.e(TAG, "Unable to recover keystore alias $keystoreUser") + return Error.ERR_CANT_ACQUIRE_RESOURCE + } + + val certChain = keyStore.getCertificateChain(keystoreUser) + if (certChain.isNullOrEmpty()) { + Log.e(TAG, "Keystore alias $keystoreUser does not contain certificates") + return Error.ERR_INVALID_DATA + } + val certs = ArrayList<X509Certificate>(certChain.size) + for (cert in certChain) { + certs.add(cert as X509Certificate) + } + + val signerConfig = ApkSigner.SignerConfig.Builder(keystoreUser, keyStoreKey, certs).build() + + val apkSigner = ApkSigner.Builder(listOf(signerConfig)) + .setInputApk(File(inputPath)) + .setOutputApk(File(tmpOutputPath)) + .build() + + try { + apkSigner.sign() + } catch (e: Exception) { + Log.e(TAG, "Unable to sign $inputPath", e) + return Error.FAILED + } + + if (outputPath != tmpOutputPath && !fileAccessHandler.renameFile(tmpOutputPath, outputPath)) { + Log.e(TAG, "Unable to rename temp output file $tmpOutputPath to $outputPath") + return Error.ERR_FILE_CANT_WRITE + } + + Log.v(TAG, "Signed $inputPath") + return Error.OK +} diff --git a/platform/android/java/editor/src/main/res/drawable/ic_play_window_foreground.xml b/platform/android/java/editor/src/main/res/drawable/ic_play_window_foreground.xml new file mode 100644 index 0000000000..41bc5475c8 --- /dev/null +++ b/platform/android/java/editor/src/main/res/drawable/ic_play_window_foreground.xml @@ -0,0 +1,25 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="108dp" + android:height="108dp" + android:tint="#FFFFFF" + android:viewportWidth="24" + android:viewportHeight="24"> + <group + android:scaleX="0.522" + android:scaleY="0.522" + android:translateX="5.736" + android:translateY="5.736"> + <path + android:fillColor="@android:color/white" + android:pathData="M21.58,16.09l-1.09,-7.66C20.21,6.46 18.52,5 16.53,5H7.47C5.48,5 3.79,6.46 3.51,8.43l-1.09,7.66C2.2,17.63 3.39,19 4.94,19h0c0.68,0 1.32,-0.27 1.8,-0.75L9,16h6l2.25,2.25c0.48,0.48 1.13,0.75 1.8,0.75h0C20.61,19 21.8,17.63 21.58,16.09zM19.48,16.81C19.4,16.9 19.27,17 19.06,17c-0.15,0 -0.29,-0.06 -0.39,-0.16L15.83,14H8.17l-2.84,2.84C5.23,16.94 5.09,17 4.94,17c-0.21,0 -0.34,-0.1 -0.42,-0.19c-0.08,-0.09 -0.16,-0.23 -0.13,-0.44l1.09,-7.66C5.63,7.74 6.48,7 7.47,7h9.06c0.99,0 1.84,0.74 1.98,1.72l1.09,7.66C19.63,16.58 19.55,16.72 19.48,16.81z" /> + <path + android:fillColor="@android:color/white" + android:pathData="M9,8l-1,0l0,2l-2,0l0,1l2,0l0,2l1,0l0,-2l2,0l0,-1l-2,0z" /> + <path + android:fillColor="@android:color/white" + android:pathData="M17,12m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0" /> + <path + android:fillColor="@android:color/white" + android:pathData="M15,9m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0" /> + </group> +</vector> diff --git a/platform/android/java/editor/src/main/res/drawable/outline_fullscreen_exit_48.xml b/platform/android/java/editor/src/main/res/drawable/outline_fullscreen_exit_48.xml new file mode 100644 index 0000000000..c8b5a15d19 --- /dev/null +++ b/platform/android/java/editor/src/main/res/drawable/outline_fullscreen_exit_48.xml @@ -0,0 +1,12 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="48dp" + android:height="48dp" + android:tint="#FFFFFF" + android:viewportWidth="24" + android:viewportHeight="24"> + + <path + android:fillColor="@android:color/white" + android:pathData="M5,16h3v3h2v-5L5,14v2zM8,8L5,8v2h5L10,5L8,5v3zM14,19h2v-3h3v-2h-5v5zM16,8L16,5h-2v5h5L19,8h-3z" /> + +</vector> diff --git a/platform/android/java/editor/src/main/res/drawable/pip_button_activated_bg_drawable.xml b/platform/android/java/editor/src/main/res/drawable/pip_button_activated_bg_drawable.xml new file mode 100644 index 0000000000..aeaa96ce54 --- /dev/null +++ b/platform/android/java/editor/src/main/res/drawable/pip_button_activated_bg_drawable.xml @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="utf-8"?> +<shape xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="oval"> + + <size + android:width="60dp" + android:height="60dp" /> + + <solid android:color="#44000000" /> +</shape> diff --git a/platform/android/java/editor/src/main/res/drawable/pip_button_bg_drawable.xml b/platform/android/java/editor/src/main/res/drawable/pip_button_bg_drawable.xml new file mode 100644 index 0000000000..e9b2959275 --- /dev/null +++ b/platform/android/java/editor/src/main/res/drawable/pip_button_bg_drawable.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + + <item android:drawable="@drawable/pip_button_activated_bg_drawable" android:state_pressed="true" /> + <item android:drawable="@drawable/pip_button_activated_bg_drawable" android:state_hovered="true" /> + + <item android:drawable="@drawable/pip_button_default_bg_drawable" /> + +</selector> diff --git a/platform/android/java/editor/src/main/res/drawable/pip_button_default_bg_drawable.xml b/platform/android/java/editor/src/main/res/drawable/pip_button_default_bg_drawable.xml new file mode 100644 index 0000000000..a8919689fe --- /dev/null +++ b/platform/android/java/editor/src/main/res/drawable/pip_button_default_bg_drawable.xml @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="utf-8"?> +<shape xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="oval"> + + <size + android:width="60dp" + android:height="60dp" /> + + <solid android:color="#13000000" /> +</shape> diff --git a/platform/android/java/editor/src/main/res/layout/godot_game_layout.xml b/platform/android/java/editor/src/main/res/layout/godot_game_layout.xml new file mode 100644 index 0000000000..d53787c87e --- /dev/null +++ b/platform/android/java/editor/src/main/res/layout/godot_game_layout.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <FrameLayout + android:id="@+id/godot_fragment_container" + android:layout_width="match_parent" + android:layout_height="match_parent"/> + + <ImageView + android:id="@+id/godot_pip_button" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_margin="36dp" + android:contentDescription="@string/pip_button_description" + android:background="@drawable/pip_button_bg_drawable" + android:scaleType="center" + android:src="@drawable/outline_fullscreen_exit_48" + android:visibility="gone" + android:layout_gravity="end|top" + tools:visibility="visible" /> + +</FrameLayout> diff --git a/platform/android/java/editor/src/main/res/mipmap-anydpi-v26/ic_play_window.xml b/platform/android/java/editor/src/main/res/mipmap-anydpi-v26/ic_play_window.xml new file mode 100644 index 0000000000..a3aabf2ee0 --- /dev/null +++ b/platform/android/java/editor/src/main/res/mipmap-anydpi-v26/ic_play_window.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> + <background android:drawable="@mipmap/icon_background"/> + <foreground android:drawable="@drawable/ic_play_window_foreground"/> +</adaptive-icon> diff --git a/platform/android/java/editor/src/main/res/mipmap-hdpi/ic_play_window.png b/platform/android/java/editor/src/main/res/mipmap-hdpi/ic_play_window.png Binary files differnew file mode 100644 index 0000000000..a5ce40241f --- /dev/null +++ b/platform/android/java/editor/src/main/res/mipmap-hdpi/ic_play_window.png diff --git a/platform/android/java/editor/src/main/res/mipmap-mdpi/ic_play_window.png b/platform/android/java/editor/src/main/res/mipmap-mdpi/ic_play_window.png Binary files differnew file mode 100644 index 0000000000..147adb6127 --- /dev/null +++ b/platform/android/java/editor/src/main/res/mipmap-mdpi/ic_play_window.png diff --git a/platform/android/java/editor/src/main/res/mipmap-xhdpi/ic_play_window.png b/platform/android/java/editor/src/main/res/mipmap-xhdpi/ic_play_window.png Binary files differnew file mode 100644 index 0000000000..0b1db1b923 --- /dev/null +++ b/platform/android/java/editor/src/main/res/mipmap-xhdpi/ic_play_window.png diff --git a/platform/android/java/editor/src/main/res/mipmap-xxhdpi/ic_play_window.png b/platform/android/java/editor/src/main/res/mipmap-xxhdpi/ic_play_window.png Binary files differnew file mode 100644 index 0000000000..39d7450390 --- /dev/null +++ b/platform/android/java/editor/src/main/res/mipmap-xxhdpi/ic_play_window.png diff --git a/platform/android/java/editor/src/main/res/mipmap-xxxhdpi/ic_play_window.png b/platform/android/java/editor/src/main/res/mipmap-xxxhdpi/ic_play_window.png Binary files differnew file mode 100644 index 0000000000..b7a09a15b5 --- /dev/null +++ b/platform/android/java/editor/src/main/res/mipmap-xxxhdpi/ic_play_window.png diff --git a/platform/android/java/editor/src/main/res/values/dimens.xml b/platform/android/java/editor/src/main/res/values/dimens.xml index 98bfe40179..1e486872e6 100644 --- a/platform/android/java/editor/src/main/res/values/dimens.xml +++ b/platform/android/java/editor/src/main/res/values/dimens.xml @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="utf-8"?> <resources> - <dimen name="editor_default_window_height">600dp</dimen> + <dimen name="editor_default_window_height">640dp</dimen> <dimen name="editor_default_window_width">1024dp</dimen> </resources> diff --git a/platform/android/java/editor/src/main/res/values/strings.xml b/platform/android/java/editor/src/main/res/values/strings.xml index 909711ab18..0ad54ac3a1 100644 --- a/platform/android/java/editor/src/main/res/values/strings.xml +++ b/platform/android/java/editor/src/main/res/values/strings.xml @@ -1,4 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <resources> + <string name="godot_game_activity_name">Godot Play window</string> <string name="denied_storage_permission_error_msg">Missing storage access permission!</string> + <string name="pip_button_description">Button used to toggle picture-in-picture mode for the Play window</string> </resources> diff --git a/platform/android/java/lib/src/org/godotengine/godot/Godot.kt b/platform/android/java/lib/src/org/godotengine/godot/Godot.kt index 111cd48405..38bd336e2d 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/Godot.kt +++ b/platform/android/java/lib/src/org/godotengine/godot/Godot.kt @@ -42,14 +42,18 @@ import android.hardware.Sensor import android.hardware.SensorManager import android.os.* import android.util.Log +import android.util.TypedValue import android.view.* import android.widget.FrameLayout import androidx.annotation.Keep import androidx.annotation.StringRes import androidx.core.view.ViewCompat +import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsAnimationCompat import androidx.core.view.WindowInsetsCompat +import androidx.core.view.WindowInsetsControllerCompat import com.google.android.vending.expansion.downloader.* +import org.godotengine.godot.error.Error import org.godotengine.godot.input.GodotEditText import org.godotengine.godot.input.GodotInputHandler import org.godotengine.godot.io.directory.DirectoryAccessHandler @@ -83,15 +87,19 @@ import java.util.concurrent.atomic.AtomicReference */ class Godot(private val context: Context) { - private companion object { + internal companion object { private val TAG = Godot::class.java.simpleName // Supported build flavors const val EDITOR_FLAVOR = "editor" const val TEMPLATE_FLAVOR = "template" + + /** + * @return true if this is an editor build, false if this is a template build + */ + fun isEditorBuild() = BuildConfig.FLAVOR == EDITOR_FLAVOR } - private val windowManager: WindowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager private val mSensorManager: SensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager private val mClipboard: ClipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager private val vibratorService: Vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator @@ -100,36 +108,26 @@ class Godot(private val context: Context) { GodotPluginRegistry.getPluginRegistry() } - private val accelerometer_enabled = AtomicBoolean(false) + private val accelerometerEnabled = AtomicBoolean(false) private val mAccelerometer: Sensor? by lazy { mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) } - private val gravity_enabled = AtomicBoolean(false) + private val gravityEnabled = AtomicBoolean(false) private val mGravity: Sensor? by lazy { mSensorManager.getDefaultSensor(Sensor.TYPE_GRAVITY) } - private val magnetometer_enabled = AtomicBoolean(false) + private val magnetometerEnabled = AtomicBoolean(false) private val mMagnetometer: Sensor? by lazy { mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD) } - private val gyroscope_enabled = AtomicBoolean(false) + private val gyroscopeEnabled = AtomicBoolean(false) private val mGyroscope: Sensor? by lazy { mSensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE) } - private val uiChangeListener = View.OnSystemUiVisibilityChangeListener { visibility: Int -> - if (visibility and View.SYSTEM_UI_FLAG_FULLSCREEN == 0) { - val decorView = requireActivity().window.decorView - decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or - View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or - View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or - View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or - View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY - }} - val tts = GodotTTS(context) val directoryAccessHandler = DirectoryAccessHandler(context) val fileAccessHandler = FileAccessHandler(context) @@ -180,7 +178,7 @@ class Godot(private val context: Context) { private var xrMode = XRMode.REGULAR private var expansionPackPath: String = "" private var useApkExpansion = false - private var useImmersive = false + private val useImmersive = AtomicBoolean(false) private var useDebugOpengl = false private var darkMode = false @@ -249,15 +247,9 @@ class Godot(private val context: Context) { xrMode = XRMode.OPENXR } else if (commandLine[i] == "--debug_opengl") { useDebugOpengl = true - } else if (commandLine[i] == "--use_immersive") { - useImmersive = true - window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or - View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or - View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or - View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or // hide nav bar - View.SYSTEM_UI_FLAG_FULLSCREEN or // hide status bar - View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY - registerUiChangeListener() + } else if (commandLine[i] == "--fullscreen") { + useImmersive.set(true) + newArgs.add(commandLine[i]) } else if (commandLine[i] == "--use_apk_expansion") { useApkExpansion = true } else if (hasExtra && commandLine[i] == "--apk_expansion_md5") { @@ -331,6 +323,54 @@ class Godot(private val context: Context) { } /** + * Toggle immersive mode. + * Must be called from the UI thread. + */ + private fun enableImmersiveMode(enabled: Boolean, override: Boolean = false) { + val activity = getActivity() ?: return + val window = activity.window ?: return + + if (!useImmersive.compareAndSet(!enabled, enabled) && !override) { + return + } + + WindowCompat.setDecorFitsSystemWindows(window, !enabled) + val controller = WindowInsetsControllerCompat(window, window.decorView) + if (enabled) { + controller.hide(WindowInsetsCompat.Type.systemBars()) + controller.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + } else { + val fullScreenThemeValue = TypedValue() + val hasStatusBar = if (activity.theme.resolveAttribute(android.R.attr.windowFullscreen, fullScreenThemeValue, true) && fullScreenThemeValue.type == TypedValue.TYPE_INT_BOOLEAN) { + fullScreenThemeValue.data == 0 + } else { + // Fallback to checking the editor build + !isEditorBuild() + } + + val types = if (hasStatusBar) { + WindowInsetsCompat.Type.navigationBars() or WindowInsetsCompat.Type.statusBars() + } else { + WindowInsetsCompat.Type.navigationBars() + } + controller.show(types) + } + } + + /** + * Invoked from the render thread to toggle the immersive mode. + */ + @Keep + private fun nativeEnableImmersiveMode(enabled: Boolean) { + runOnUiThread { + enableImmersiveMode(enabled) + } + } + + @Keep + fun isInImmersiveMode() = useImmersive.get() + + /** * Initializes the native layer of the Godot engine. * * This must be preceded by [onCreate] and followed by [onInitRenderView] to complete @@ -547,15 +587,7 @@ class Godot(private val context: Context) { renderView?.onActivityResumed() registerSensorsIfNeeded() - if (useImmersive) { - val window = requireActivity().window - window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or - View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or - View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or - View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or // hide nav bar - View.SYSTEM_UI_FLAG_FULLSCREEN or // hide status bar - View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY - } + enableImmersiveMode(useImmersive.get(), true) for (plugin in pluginRegistry.allPlugins) { plugin.onMainResume() } @@ -566,16 +598,16 @@ class Godot(private val context: Context) { return } - if (accelerometer_enabled.get() && mAccelerometer != null) { + if (accelerometerEnabled.get() && mAccelerometer != null) { mSensorManager.registerListener(godotInputHandler, mAccelerometer, SensorManager.SENSOR_DELAY_GAME) } - if (gravity_enabled.get() && mGravity != null) { + if (gravityEnabled.get() && mGravity != null) { mSensorManager.registerListener(godotInputHandler, mGravity, SensorManager.SENSOR_DELAY_GAME) } - if (magnetometer_enabled.get() && mMagnetometer != null) { + if (magnetometerEnabled.get() && mMagnetometer != null) { mSensorManager.registerListener(godotInputHandler, mMagnetometer, SensorManager.SENSOR_DELAY_GAME) } - if (gyroscope_enabled.get() && mGyroscope != null) { + if (gyroscopeEnabled.get() && mGyroscope != null) { mSensorManager.registerListener(godotInputHandler, mGyroscope, SensorManager.SENSOR_DELAY_GAME) } } @@ -691,10 +723,10 @@ class Godot(private val context: Context) { Log.v(TAG, "OnGodotMainLoopStarted") godotMainLoopStarted.set(true) - accelerometer_enabled.set(java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/sensors/enable_accelerometer"))) - gravity_enabled.set(java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/sensors/enable_gravity"))) - gyroscope_enabled.set(java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/sensors/enable_gyroscope"))) - magnetometer_enabled.set(java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/sensors/enable_magnetometer"))) + accelerometerEnabled.set(java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/sensors/enable_accelerometer"))) + gravityEnabled.set(java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/sensors/enable_gravity"))) + gyroscopeEnabled.set(java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/sensors/enable_gyroscope"))) + magnetometerEnabled.set(java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/sensors/enable_magnetometer"))) runOnUiThread { registerSensorsIfNeeded() @@ -719,11 +751,6 @@ class Godot(private val context: Context) { primaryHost?.onGodotRestartRequested(this) } - private fun registerUiChangeListener() { - val decorView = requireActivity().window.decorView - decorView.setOnSystemUiVisibilityChangeListener(uiChangeListener) - } - fun alert( @StringRes messageResId: Int, @StringRes titleResId: Int, @@ -834,11 +861,6 @@ class Godot(private val context: Context) { return mClipboard.hasPrimaryClip() } - /** - * @return true if this is an editor build, false if this is a template build - */ - fun isEditorBuild() = BuildConfig.FLAVOR == EDITOR_FLAVOR - fun getClipboard(): String { val clipData = mClipboard.primaryClip ?: return "" val text = clipData.getItemAt(0).text ?: return "" @@ -1054,4 +1076,20 @@ class Godot(private val context: Context) { private fun nativeDumpBenchmark(benchmarkFile: String) { dumpBenchmark(fileAccessHandler, benchmarkFile) } + + @Keep + private fun nativeSignApk(inputPath: String, + outputPath: String, + keystorePath: String, + keystoreUser: String, + keystorePassword: String): Int { + val signResult = primaryHost?.signApk(inputPath, outputPath, keystorePath, keystoreUser, keystorePassword) ?: Error.ERR_UNAVAILABLE + return signResult.toNativeValue() + } + + @Keep + private fun nativeVerifyApk(apkPath: String): Int { + val verifyResult = primaryHost?.verifyApk(apkPath) ?: Error.ERR_UNAVAILABLE + return verifyResult.toNativeValue() + } } diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotActivity.kt b/platform/android/java/lib/src/org/godotengine/godot/GodotActivity.kt index 913e3d04c5..474c6e9b2f 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotActivity.kt +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotActivity.kt @@ -53,8 +53,6 @@ abstract class GodotActivity : FragmentActivity(), GodotHost { private val TAG = GodotActivity::class.java.simpleName @JvmStatic - protected val EXTRA_FORCE_QUIT = "force_quit_requested" - @JvmStatic protected val EXTRA_NEW_LAUNCH = "new_launch_requested" } @@ -128,12 +126,6 @@ abstract class GodotActivity : FragmentActivity(), GodotHost { } private fun handleStartIntent(intent: Intent, newLaunch: Boolean) { - val forceQuitRequested = intent.getBooleanExtra(EXTRA_FORCE_QUIT, false) - if (forceQuitRequested) { - Log.d(TAG, "Force quit requested, terminating..") - ProcessPhoenix.forceQuit(this) - return - } if (!newLaunch) { val newLaunchRequested = intent.getBooleanExtra(EXTRA_NEW_LAUNCH, false) if (newLaunchRequested) { diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotFragment.java b/platform/android/java/lib/src/org/godotengine/godot/GodotFragment.java index fdda766594..e0f5744368 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotFragment.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotFragment.java @@ -30,6 +30,7 @@ package org.godotengine.godot; +import org.godotengine.godot.error.Error; import org.godotengine.godot.plugin.GodotPlugin; import org.godotengine.godot.utils.BenchmarkUtils; @@ -484,4 +485,20 @@ public class GodotFragment extends Fragment implements IDownloaderClient, GodotH } return Collections.emptySet(); } + + @Override + public Error signApk(@NonNull String inputPath, @NonNull String outputPath, @NonNull String keystorePath, @NonNull String keystoreUser, @NonNull String keystorePassword) { + if (parentHost != null) { + return parentHost.signApk(inputPath, outputPath, keystorePath, keystoreUser, keystorePassword); + } + return Error.ERR_UNAVAILABLE; + } + + @Override + public Error verifyApk(@NonNull String apkPath) { + if (parentHost != null) { + return parentHost.verifyApk(apkPath); + } + return Error.ERR_UNAVAILABLE; + } } diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotHost.java b/platform/android/java/lib/src/org/godotengine/godot/GodotHost.java index 1862b9fa9b..f1c84e90a7 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotHost.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotHost.java @@ -30,10 +30,13 @@ package org.godotengine.godot; +import org.godotengine.godot.error.Error; import org.godotengine.godot.plugin.GodotPlugin; import android.app.Activity; +import androidx.annotation.NonNull; + import java.util.Collections; import java.util.List; import java.util.Set; @@ -108,4 +111,29 @@ public interface GodotHost { default Set<GodotPlugin> getHostPlugins(Godot engine) { return Collections.emptySet(); } + + /** + * Signs the given Android apk + * + * @param inputPath Path to the apk that should be signed + * @param outputPath Path for the signed output apk; can be the same as inputPath + * @param keystorePath Path to the keystore to use for signing the apk + * @param keystoreUser Keystore user credential + * @param keystorePassword Keystore password credential + * + * @return {@link Error#OK} if signing is successful + */ + default Error signApk(@NonNull String inputPath, @NonNull String outputPath, @NonNull String keystorePath, @NonNull String keystoreUser, @NonNull String keystorePassword) { + return Error.ERR_UNAVAILABLE; + } + + /** + * Verifies the given Android apk is signed + * + * @param apkPath Path to the apk that should be verified + * @return {@link Error#OK} if verification was successful + */ + default Error verifyApk(@NonNull String apkPath) { + return Error.ERR_UNAVAILABLE; + } } diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java b/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java index 909daf05c9..295a4a6340 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java @@ -246,4 +246,9 @@ public class GodotLib { * dispatched from the UI thread. */ public static native boolean shouldDispatchInputToRenderThread(); + + /** + * @return the project resource directory + */ + public static native String getProjectResourceDir(); } diff --git a/platform/android/java/lib/src/org/godotengine/godot/error/Error.kt b/platform/android/java/lib/src/org/godotengine/godot/error/Error.kt new file mode 100644 index 0000000000..00ef5ee341 --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/error/Error.kt @@ -0,0 +1,100 @@ +/**************************************************************************/ +/* Error.kt */ +/**************************************************************************/ +/* 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. */ +/**************************************************************************/ + +package org.godotengine.godot.error + +/** + * Godot error list. + * + * This enum MUST match its native counterpart in 'core/error/error_list.h' + */ +enum class Error(private val description: String) { + OK("OK"), // (0) + FAILED("Failed"), ///< Generic fail error + ERR_UNAVAILABLE("Unavailable"), ///< What is requested is unsupported/unavailable + ERR_UNCONFIGURED("Unconfigured"), ///< The object being used hasn't been properly set up yet + ERR_UNAUTHORIZED("Unauthorized"), ///< Missing credentials for requested resource + ERR_PARAMETER_RANGE_ERROR("Parameter out of range"), ///< Parameter given out of range (5) + ERR_OUT_OF_MEMORY("Out of memory"), ///< Out of memory + ERR_FILE_NOT_FOUND("File not found"), + ERR_FILE_BAD_DRIVE("File: Bad drive"), + ERR_FILE_BAD_PATH("File: Bad path"), + ERR_FILE_NO_PERMISSION("File: Permission denied"), // (10) + ERR_FILE_ALREADY_IN_USE("File already in use"), + ERR_FILE_CANT_OPEN("Can't open file"), + ERR_FILE_CANT_WRITE("Can't write file"), + ERR_FILE_CANT_READ("Can't read file"), + ERR_FILE_UNRECOGNIZED("File unrecognized"), // (15) + ERR_FILE_CORRUPT("File corrupt"), + ERR_FILE_MISSING_DEPENDENCIES("Missing dependencies for file"), + ERR_FILE_EOF("End of file"), + ERR_CANT_OPEN("Can't open"), ///< Can't open a resource/socket/file + ERR_CANT_CREATE("Can't create"), // (20) + ERR_QUERY_FAILED("Query failed"), + ERR_ALREADY_IN_USE("Already in use"), + ERR_LOCKED("Locked"), ///< resource is locked + ERR_TIMEOUT("Timeout"), + ERR_CANT_CONNECT("Can't connect"), // (25) + ERR_CANT_RESOLVE("Can't resolve"), + ERR_CONNECTION_ERROR("Connection error"), + ERR_CANT_ACQUIRE_RESOURCE("Can't acquire resource"), + ERR_CANT_FORK("Can't fork"), + ERR_INVALID_DATA("Invalid data"), ///< Data passed is invalid (30) + ERR_INVALID_PARAMETER("Invalid parameter"), ///< Parameter passed is invalid + ERR_ALREADY_EXISTS("Already exists"), ///< When adding, item already exists + ERR_DOES_NOT_EXIST("Does not exist"), ///< When retrieving/erasing, if item does not exist + ERR_DATABASE_CANT_READ("Can't read database"), ///< database is full + ERR_DATABASE_CANT_WRITE("Can't write database"), ///< database is full (35) + ERR_COMPILATION_FAILED("Compilation failed"), + ERR_METHOD_NOT_FOUND("Method not found"), + ERR_LINK_FAILED("Link failed"), + ERR_SCRIPT_FAILED("Script failed"), + ERR_CYCLIC_LINK("Cyclic link detected"), // (40) + ERR_INVALID_DECLARATION("Invalid declaration"), + ERR_DUPLICATE_SYMBOL("Duplicate symbol"), + ERR_PARSE_ERROR("Parse error"), + ERR_BUSY("Busy"), + ERR_SKIP("Skip"), // (45) + ERR_HELP("Help"), ///< user requested help!! + ERR_BUG("Bug"), ///< a bug in the software certainly happened, due to a double check failing or unexpected behavior. + ERR_PRINTER_ON_FIRE("Printer on fire"); /// the parallel port printer is engulfed in flames + + companion object { + internal fun fromNativeValue(nativeValue: Int): Error? { + return Error.entries.getOrNull(nativeValue) + } + } + + internal fun toNativeValue(): Int = this.ordinal + + override fun toString(): String { + return description + } +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/io/StorageScope.kt b/platform/android/java/lib/src/org/godotengine/godot/io/StorageScope.kt index 8ee3d5f48f..574ecd58eb 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/io/StorageScope.kt +++ b/platform/android/java/lib/src/org/godotengine/godot/io/StorageScope.kt @@ -34,12 +34,18 @@ import android.content.Context import android.os.Build import android.os.Environment import java.io.File +import org.godotengine.godot.GodotLib /** * Represents the different storage scopes. */ internal enum class StorageScope { /** + * Covers the 'assets' directory + */ + ASSETS, + + /** * Covers internal and external directories accessible to the app without restrictions. */ APP, @@ -56,6 +62,10 @@ internal enum class StorageScope { class Identifier(context: Context) { + companion object { + internal const val ASSETS_PREFIX = "assets://" + } + private val internalAppDir: String? = context.filesDir.canonicalPath private val internalCacheDir: String? = context.cacheDir.canonicalPath private val externalAppDir: String? = context.getExternalFilesDir(null)?.canonicalPath @@ -64,6 +74,14 @@ internal enum class StorageScope { private val documentsSharedDir: String? = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS).canonicalPath /** + * Determine if the given path is accessible. + */ + fun canAccess(path: String?): Boolean { + val storageScope = identifyStorageScope(path) + return storageScope == APP || storageScope == SHARED + } + + /** * Determines which [StorageScope] the given path falls under. */ fun identifyStorageScope(path: String?): StorageScope { @@ -71,9 +89,16 @@ internal enum class StorageScope { return UNKNOWN } - val pathFile = File(path) + if (path.startsWith(ASSETS_PREFIX)) { + return ASSETS + } + + var pathFile = File(path) if (!pathFile.isAbsolute) { - return UNKNOWN + pathFile = File(GodotLib.getProjectResourceDir(), path) + if (!pathFile.isAbsolute) { + return UNKNOWN + } } // If we have 'All Files Access' permission, we can access all directories without diff --git a/platform/android/java/lib/src/org/godotengine/godot/io/directory/AssetsDirectoryAccess.kt b/platform/android/java/lib/src/org/godotengine/godot/io/directory/AssetsDirectoryAccess.kt index b9b7ebac6e..523e852518 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/io/directory/AssetsDirectoryAccess.kt +++ b/platform/android/java/lib/src/org/godotengine/godot/io/directory/AssetsDirectoryAccess.kt @@ -33,18 +33,30 @@ package org.godotengine.godot.io.directory import android.content.Context import android.util.Log import android.util.SparseArray +import org.godotengine.godot.io.StorageScope import org.godotengine.godot.io.directory.DirectoryAccessHandler.Companion.INVALID_DIR_ID import org.godotengine.godot.io.directory.DirectoryAccessHandler.Companion.STARTING_DIR_ID +import org.godotengine.godot.io.file.AssetData import java.io.File import java.io.IOException /** * Handles directories access within the Android assets directory. */ -internal class AssetsDirectoryAccess(context: Context) : DirectoryAccessHandler.DirectoryAccess { +internal class AssetsDirectoryAccess(private val context: Context) : DirectoryAccessHandler.DirectoryAccess { companion object { private val TAG = AssetsDirectoryAccess::class.java.simpleName + + internal fun getAssetsPath(originalPath: String): String { + if (originalPath.startsWith(File.separator)) { + return originalPath.substring(File.separator.length) + } + if (originalPath.startsWith(StorageScope.Identifier.ASSETS_PREFIX)) { + return originalPath.substring(StorageScope.Identifier.ASSETS_PREFIX.length) + } + return originalPath + } } private data class AssetDir(val path: String, val files: Array<String>, var current: Int = 0) @@ -54,13 +66,6 @@ internal class AssetsDirectoryAccess(context: Context) : DirectoryAccessHandler. private var lastDirId = STARTING_DIR_ID private val dirs = SparseArray<AssetDir>() - private fun getAssetsPath(originalPath: String): String { - if (originalPath.startsWith(File.separatorChar)) { - return originalPath.substring(1) - } - return originalPath - } - override fun hasDirId(dirId: Int) = dirs.indexOfKey(dirId) >= 0 override fun dirOpen(path: String): Int { @@ -68,8 +73,8 @@ internal class AssetsDirectoryAccess(context: Context) : DirectoryAccessHandler. try { val files = assetManager.list(assetsPath) ?: return INVALID_DIR_ID // Empty directories don't get added to the 'assets' directory, so - // if ad.files.length > 0 ==> path is directory - // if ad.files.length == 0 ==> path is file + // if files.length > 0 ==> path is directory + // if files.length == 0 ==> path is file if (files.isEmpty()) { return INVALID_DIR_ID } @@ -89,8 +94,8 @@ internal class AssetsDirectoryAccess(context: Context) : DirectoryAccessHandler. try { val files = assetManager.list(assetsPath) ?: return false // Empty directories don't get added to the 'assets' directory, so - // if ad.files.length > 0 ==> path is directory - // if ad.files.length == 0 ==> path is file + // if files.length > 0 ==> path is directory + // if files.length == 0 ==> path is file return files.isNotEmpty() } catch (e: IOException) { Log.e(TAG, "Exception on dirExists", e) @@ -98,19 +103,7 @@ internal class AssetsDirectoryAccess(context: Context) : DirectoryAccessHandler. } } - override fun fileExists(path: String): Boolean { - val assetsPath = getAssetsPath(path) - try { - val files = assetManager.list(assetsPath) ?: return false - // Empty directories don't get added to the 'assets' directory, so - // if ad.files.length > 0 ==> path is directory - // if ad.files.length == 0 ==> path is file - return files.isEmpty() - } catch (e: IOException) { - Log.e(TAG, "Exception on fileExists", e) - return false - } - } + override fun fileExists(path: String) = AssetData.fileExists(context, path) override fun dirIsDir(dirId: Int): Boolean { val ad: AssetDir = dirs[dirId] @@ -171,7 +164,7 @@ internal class AssetsDirectoryAccess(context: Context) : DirectoryAccessHandler. override fun getSpaceLeft() = 0L - override fun rename(from: String, to: String) = false + override fun rename(from: String, to: String) = AssetData.rename(from, to) - override fun remove(filename: String) = false + override fun remove(filename: String) = AssetData.delete(filename) } diff --git a/platform/android/java/lib/src/org/godotengine/godot/io/directory/DirectoryAccessHandler.kt b/platform/android/java/lib/src/org/godotengine/godot/io/directory/DirectoryAccessHandler.kt index dd6d5180c5..9f3461200b 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/io/directory/DirectoryAccessHandler.kt +++ b/platform/android/java/lib/src/org/godotengine/godot/io/directory/DirectoryAccessHandler.kt @@ -32,7 +32,8 @@ package org.godotengine.godot.io.directory import android.content.Context import android.util.Log -import org.godotengine.godot.io.directory.DirectoryAccessHandler.AccessType.ACCESS_FILESYSTEM +import org.godotengine.godot.Godot +import org.godotengine.godot.io.StorageScope import org.godotengine.godot.io.directory.DirectoryAccessHandler.AccessType.ACCESS_RESOURCES /** @@ -45,18 +46,82 @@ class DirectoryAccessHandler(context: Context) { internal const val INVALID_DIR_ID = -1 internal const val STARTING_DIR_ID = 1 - - private fun getAccessTypeFromNative(accessType: Int): AccessType? { - return when (accessType) { - ACCESS_RESOURCES.nativeValue -> ACCESS_RESOURCES - ACCESS_FILESYSTEM.nativeValue -> ACCESS_FILESYSTEM - else -> null - } - } } private enum class AccessType(val nativeValue: Int) { - ACCESS_RESOURCES(0), ACCESS_FILESYSTEM(2) + ACCESS_RESOURCES(0), + + /** + * Maps to [ACCESS_FILESYSTEM] + */ + ACCESS_USERDATA(1), + ACCESS_FILESYSTEM(2); + + fun generateDirAccessId(dirId: Int) = (dirId * DIR_ACCESS_ID_MULTIPLIER) + nativeValue + + companion object { + const val DIR_ACCESS_ID_MULTIPLIER = 10 + + fun fromDirAccessId(dirAccessId: Int): Pair<AccessType?, Int> { + val nativeValue = dirAccessId % DIR_ACCESS_ID_MULTIPLIER + val dirId = dirAccessId / DIR_ACCESS_ID_MULTIPLIER + return Pair(fromNative(nativeValue), dirId) + } + + private fun fromNative(nativeAccessType: Int): AccessType? { + for (accessType in entries) { + if (accessType.nativeValue == nativeAccessType) { + return accessType + } + } + return null + } + + fun fromNative(nativeAccessType: Int, storageScope: StorageScope? = null): AccessType? { + val accessType = fromNative(nativeAccessType) + if (accessType == null) { + Log.w(TAG, "Unsupported access type $nativeAccessType") + return null + } + + // 'Resources' access type takes precedence as it is simple to handle: + // if we receive a 'Resources' access type and this is a template build, + // we provide a 'Resources' directory handler. + // If this is an editor build, 'Resources' refers to the opened project resources + // and so we provide a 'Filesystem' directory handler. + if (accessType == ACCESS_RESOURCES) { + return if (Godot.isEditorBuild()) { + ACCESS_FILESYSTEM + } else { + ACCESS_RESOURCES + } + } else { + // We've received a 'Filesystem' or 'Userdata' access type. On Android, this + // may refer to: + // - assets directory (path has 'assets:/' prefix) + // - app directories + // - device shared directories + // As such we check the storage scope (if available) to figure what type of + // directory handler to provide + if (storageScope != null) { + val accessTypeFromStorageScope = when (storageScope) { + StorageScope.ASSETS -> ACCESS_RESOURCES + StorageScope.APP, StorageScope.SHARED -> ACCESS_FILESYSTEM + StorageScope.UNKNOWN -> null + } + + if (accessTypeFromStorageScope != null) { + return accessTypeFromStorageScope + } + } + // If we're not able to infer the type of directory handler from the storage + // scope, we fall-back to the 'Filesystem' directory handler as it's the default + // for the 'Filesystem' access type. + // Note that ACCESS_USERDATA also maps to ACCESS_FILESYSTEM + return ACCESS_FILESYSTEM + } + } + } } internal interface DirectoryAccess { @@ -76,8 +141,10 @@ class DirectoryAccessHandler(context: Context) { fun remove(filename: String): Boolean } + private val storageScopeIdentifier = StorageScope.Identifier(context) + private val assetsDirAccess = AssetsDirectoryAccess(context) - private val fileSystemDirAccess = FilesystemDirectoryAccess(context) + private val fileSystemDirAccess = FilesystemDirectoryAccess(context, storageScopeIdentifier) fun assetsFileExists(assetsPath: String) = assetsDirAccess.fileExists(assetsPath) fun filesystemFileExists(path: String) = fileSystemDirAccess.fileExists(path) @@ -85,24 +152,32 @@ class DirectoryAccessHandler(context: Context) { private fun hasDirId(accessType: AccessType, dirId: Int): Boolean { return when (accessType) { ACCESS_RESOURCES -> assetsDirAccess.hasDirId(dirId) - ACCESS_FILESYSTEM -> fileSystemDirAccess.hasDirId(dirId) + else -> fileSystemDirAccess.hasDirId(dirId) } } fun dirOpen(nativeAccessType: Int, path: String?): Int { - val accessType = getAccessTypeFromNative(nativeAccessType) - if (path == null || accessType == null) { + if (path == null) { return INVALID_DIR_ID } - return when (accessType) { + val storageScope = storageScopeIdentifier.identifyStorageScope(path) + val accessType = AccessType.fromNative(nativeAccessType, storageScope) ?: return INVALID_DIR_ID + + val dirId = when (accessType) { ACCESS_RESOURCES -> assetsDirAccess.dirOpen(path) - ACCESS_FILESYSTEM -> fileSystemDirAccess.dirOpen(path) + else -> fileSystemDirAccess.dirOpen(path) + } + if (dirId == INVALID_DIR_ID) { + return INVALID_DIR_ID } + + val dirAccessId = accessType.generateDirAccessId(dirId) + return dirAccessId } - fun dirNext(nativeAccessType: Int, dirId: Int): String { - val accessType = getAccessTypeFromNative(nativeAccessType) + fun dirNext(dirAccessId: Int): String { + val (accessType, dirId) = AccessType.fromDirAccessId(dirAccessId) if (accessType == null || !hasDirId(accessType, dirId)) { Log.w(TAG, "dirNext: Invalid dir id: $dirId") return "" @@ -110,12 +185,12 @@ class DirectoryAccessHandler(context: Context) { return when (accessType) { ACCESS_RESOURCES -> assetsDirAccess.dirNext(dirId) - ACCESS_FILESYSTEM -> fileSystemDirAccess.dirNext(dirId) + else -> fileSystemDirAccess.dirNext(dirId) } } - fun dirClose(nativeAccessType: Int, dirId: Int) { - val accessType = getAccessTypeFromNative(nativeAccessType) + fun dirClose(dirAccessId: Int) { + val (accessType, dirId) = AccessType.fromDirAccessId(dirAccessId) if (accessType == null || !hasDirId(accessType, dirId)) { Log.w(TAG, "dirClose: Invalid dir id: $dirId") return @@ -123,12 +198,12 @@ class DirectoryAccessHandler(context: Context) { when (accessType) { ACCESS_RESOURCES -> assetsDirAccess.dirClose(dirId) - ACCESS_FILESYSTEM -> fileSystemDirAccess.dirClose(dirId) + else -> fileSystemDirAccess.dirClose(dirId) } } - fun dirIsDir(nativeAccessType: Int, dirId: Int): Boolean { - val accessType = getAccessTypeFromNative(nativeAccessType) + fun dirIsDir(dirAccessId: Int): Boolean { + val (accessType, dirId) = AccessType.fromDirAccessId(dirAccessId) if (accessType == null || !hasDirId(accessType, dirId)) { Log.w(TAG, "dirIsDir: Invalid dir id: $dirId") return false @@ -136,91 +211,106 @@ class DirectoryAccessHandler(context: Context) { return when (accessType) { ACCESS_RESOURCES -> assetsDirAccess.dirIsDir(dirId) - ACCESS_FILESYSTEM -> fileSystemDirAccess.dirIsDir(dirId) + else -> fileSystemDirAccess.dirIsDir(dirId) } } - fun isCurrentHidden(nativeAccessType: Int, dirId: Int): Boolean { - val accessType = getAccessTypeFromNative(nativeAccessType) + fun isCurrentHidden(dirAccessId: Int): Boolean { + val (accessType, dirId) = AccessType.fromDirAccessId(dirAccessId) if (accessType == null || !hasDirId(accessType, dirId)) { return false } return when (accessType) { ACCESS_RESOURCES -> assetsDirAccess.isCurrentHidden(dirId) - ACCESS_FILESYSTEM -> fileSystemDirAccess.isCurrentHidden(dirId) + else -> fileSystemDirAccess.isCurrentHidden(dirId) } } fun dirExists(nativeAccessType: Int, path: String?): Boolean { - val accessType = getAccessTypeFromNative(nativeAccessType) - if (path == null || accessType == null) { + if (path == null) { return false } + val storageScope = storageScopeIdentifier.identifyStorageScope(path) + val accessType = AccessType.fromNative(nativeAccessType, storageScope) ?: return false + return when (accessType) { ACCESS_RESOURCES -> assetsDirAccess.dirExists(path) - ACCESS_FILESYSTEM -> fileSystemDirAccess.dirExists(path) + else -> fileSystemDirAccess.dirExists(path) } } fun fileExists(nativeAccessType: Int, path: String?): Boolean { - val accessType = getAccessTypeFromNative(nativeAccessType) - if (path == null || accessType == null) { + if (path == null) { return false } + val storageScope = storageScopeIdentifier.identifyStorageScope(path) + val accessType = AccessType.fromNative(nativeAccessType, storageScope) ?: return false + return when (accessType) { ACCESS_RESOURCES -> assetsDirAccess.fileExists(path) - ACCESS_FILESYSTEM -> fileSystemDirAccess.fileExists(path) + else -> fileSystemDirAccess.fileExists(path) } } fun getDriveCount(nativeAccessType: Int): Int { - val accessType = getAccessTypeFromNative(nativeAccessType) ?: return 0 + val accessType = AccessType.fromNative(nativeAccessType) ?: return 0 return when(accessType) { ACCESS_RESOURCES -> assetsDirAccess.getDriveCount() - ACCESS_FILESYSTEM -> fileSystemDirAccess.getDriveCount() + else -> fileSystemDirAccess.getDriveCount() } } fun getDrive(nativeAccessType: Int, drive: Int): String { - val accessType = getAccessTypeFromNative(nativeAccessType) ?: return "" + val accessType = AccessType.fromNative(nativeAccessType) ?: return "" return when (accessType) { ACCESS_RESOURCES -> assetsDirAccess.getDrive(drive) - ACCESS_FILESYSTEM -> fileSystemDirAccess.getDrive(drive) + else -> fileSystemDirAccess.getDrive(drive) } } - fun makeDir(nativeAccessType: Int, dir: String): Boolean { - val accessType = getAccessTypeFromNative(nativeAccessType) ?: return false + fun makeDir(nativeAccessType: Int, dir: String?): Boolean { + if (dir == null) { + return false + } + + val storageScope = storageScopeIdentifier.identifyStorageScope(dir) + val accessType = AccessType.fromNative(nativeAccessType, storageScope) ?: return false + return when (accessType) { ACCESS_RESOURCES -> assetsDirAccess.makeDir(dir) - ACCESS_FILESYSTEM -> fileSystemDirAccess.makeDir(dir) + else -> fileSystemDirAccess.makeDir(dir) } } fun getSpaceLeft(nativeAccessType: Int): Long { - val accessType = getAccessTypeFromNative(nativeAccessType) ?: return 0L + val accessType = AccessType.fromNative(nativeAccessType) ?: return 0L return when (accessType) { ACCESS_RESOURCES -> assetsDirAccess.getSpaceLeft() - ACCESS_FILESYSTEM -> fileSystemDirAccess.getSpaceLeft() + else -> fileSystemDirAccess.getSpaceLeft() } } fun rename(nativeAccessType: Int, from: String, to: String): Boolean { - val accessType = getAccessTypeFromNative(nativeAccessType) ?: return false + val accessType = AccessType.fromNative(nativeAccessType) ?: return false return when (accessType) { ACCESS_RESOURCES -> assetsDirAccess.rename(from, to) - ACCESS_FILESYSTEM -> fileSystemDirAccess.rename(from, to) + else -> fileSystemDirAccess.rename(from, to) } } - fun remove(nativeAccessType: Int, filename: String): Boolean { - val accessType = getAccessTypeFromNative(nativeAccessType) ?: return false + fun remove(nativeAccessType: Int, filename: String?): Boolean { + if (filename == null) { + return false + } + + val storageScope = storageScopeIdentifier.identifyStorageScope(filename) + val accessType = AccessType.fromNative(nativeAccessType, storageScope) ?: return false return when (accessType) { ACCESS_RESOURCES -> assetsDirAccess.remove(filename) - ACCESS_FILESYSTEM -> fileSystemDirAccess.remove(filename) + else -> fileSystemDirAccess.remove(filename) } } diff --git a/platform/android/java/lib/src/org/godotengine/godot/io/directory/FilesystemDirectoryAccess.kt b/platform/android/java/lib/src/org/godotengine/godot/io/directory/FilesystemDirectoryAccess.kt index c8b4f79f30..2830216e12 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/io/directory/FilesystemDirectoryAccess.kt +++ b/platform/android/java/lib/src/org/godotengine/godot/io/directory/FilesystemDirectoryAccess.kt @@ -45,7 +45,7 @@ import java.io.File /** * Handles directories access with the internal and external filesystem. */ -internal class FilesystemDirectoryAccess(private val context: Context): +internal class FilesystemDirectoryAccess(private val context: Context, private val storageScopeIdentifier: StorageScope.Identifier): DirectoryAccessHandler.DirectoryAccess { companion object { @@ -54,7 +54,6 @@ internal class FilesystemDirectoryAccess(private val context: Context): private data class DirData(val dirFile: File, val files: Array<File>, var current: Int = 0) - private val storageScopeIdentifier = StorageScope.Identifier(context) private val storageManager = context.getSystemService(Context.STORAGE_SERVICE) as StorageManager private var lastDirId = STARTING_DIR_ID private val dirs = SparseArray<DirData>() @@ -63,7 +62,8 @@ internal class FilesystemDirectoryAccess(private val context: Context): // Directory access is available for shared storage on Android 11+ // On Android 10, access is also available as long as the `requestLegacyExternalStorage` // tag is available. - return storageScopeIdentifier.identifyStorageScope(path) != StorageScope.UNKNOWN + val storageScope = storageScopeIdentifier.identifyStorageScope(path) + return storageScope != StorageScope.UNKNOWN && storageScope != StorageScope.ASSETS } override fun hasDirId(dirId: Int) = dirs.indexOfKey(dirId) >= 0 diff --git a/platform/android/java/lib/src/org/godotengine/godot/io/file/AssetData.kt b/platform/android/java/lib/src/org/godotengine/godot/io/file/AssetData.kt new file mode 100644 index 0000000000..1ab739d90b --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/io/file/AssetData.kt @@ -0,0 +1,151 @@ +/**************************************************************************/ +/* AssetData.kt */ +/**************************************************************************/ +/* 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. */ +/**************************************************************************/ + +package org.godotengine.godot.io.file + +import android.content.Context +import android.content.res.AssetManager +import android.util.Log +import org.godotengine.godot.error.Error +import org.godotengine.godot.io.directory.AssetsDirectoryAccess +import java.io.IOException +import java.io.InputStream +import java.lang.UnsupportedOperationException +import java.nio.ByteBuffer +import java.nio.channels.Channels +import java.nio.channels.ReadableByteChannel + +/** + * Implementation of the [DataAccess] which handles access and interaction with files in the + * 'assets' directory + */ +internal class AssetData(context: Context, private val filePath: String, accessFlag: FileAccessFlags) : DataAccess() { + + companion object { + private val TAG = AssetData::class.java.simpleName + + fun fileExists(context: Context, path: String): Boolean { + val assetsPath = AssetsDirectoryAccess.getAssetsPath(path) + try { + val files = context.assets.list(assetsPath) ?: return false + // Empty directories don't get added to the 'assets' directory, so + // if files.length > 0 ==> path is directory + // if files.length == 0 ==> path is file + return files.isEmpty() + } catch (e: IOException) { + Log.e(TAG, "Exception on fileExists", e) + return false + } + } + + fun fileLastModified(path: String) = 0L + + fun delete(path: String) = false + + fun rename(from: String, to: String) = false + } + + private val inputStream: InputStream + internal val readChannel: ReadableByteChannel + + private var position = 0L + private val length: Long + + init { + if (accessFlag == FileAccessFlags.WRITE) { + throw UnsupportedOperationException("Writing to the 'assets' directory is not supported") + } + + val assetsPath = AssetsDirectoryAccess.getAssetsPath(filePath) + inputStream = context.assets.open(assetsPath, AssetManager.ACCESS_BUFFER) + readChannel = Channels.newChannel(inputStream) + + length = inputStream.available().toLong() + } + + override fun close() { + try { + inputStream.close() + } catch (e: IOException) { + Log.w(TAG, "Exception when closing file $filePath.", e) + } + } + + override fun flush() { + Log.w(TAG, "flush() is not supported.") + } + + override fun seek(position: Long) { + try { + inputStream.skip(position) + + this.position = position + if (this.position > length) { + this.position = length + endOfFile = true + } else { + endOfFile = false + } + + } catch(e: IOException) { + Log.w(TAG, "Exception when seeking file $filePath.", e) + } + } + + override fun resize(length: Long): Error { + Log.w(TAG, "resize() is not supported.") + return Error.ERR_UNAVAILABLE + } + + override fun position() = position + + override fun size() = length + + override fun read(buffer: ByteBuffer): Int { + return try { + val readBytes = readChannel.read(buffer) + if (readBytes == -1) { + endOfFile = true + 0 + } else { + position += readBytes + endOfFile = position() >= size() + readBytes + } + } catch (e: IOException) { + Log.w(TAG, "Exception while reading from $filePath.", e) + 0 + } + } + + override fun write(buffer: ByteBuffer) { + Log.w(TAG, "write() is not supported.") + } +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/io/file/DataAccess.kt b/platform/android/java/lib/src/org/godotengine/godot/io/file/DataAccess.kt index 11cf7b3566..73f020f249 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/io/file/DataAccess.kt +++ b/platform/android/java/lib/src/org/godotengine/godot/io/file/DataAccess.kt @@ -33,12 +33,17 @@ package org.godotengine.godot.io.file import android.content.Context import android.os.Build import android.util.Log +import org.godotengine.godot.error.Error import org.godotengine.godot.io.StorageScope +import java.io.FileNotFoundException import java.io.IOException +import java.io.InputStream import java.nio.ByteBuffer +import java.nio.channels.Channels import java.nio.channels.ClosedChannelException import java.nio.channels.FileChannel import java.nio.channels.NonWritableChannelException +import kotlin.jvm.Throws import kotlin.math.max /** @@ -47,11 +52,37 @@ import kotlin.math.max * Its derived instances provide concrete implementations to handle regular file access, as well * as file access through the media store API on versions of Android were scoped storage is enabled. */ -internal abstract class DataAccess(private val filePath: String) { +internal abstract class DataAccess { companion object { private val TAG = DataAccess::class.java.simpleName + @Throws(java.lang.Exception::class, FileNotFoundException::class) + fun getInputStream(storageScope: StorageScope, context: Context, filePath: String): InputStream? { + return when(storageScope) { + StorageScope.ASSETS -> { + val assetData = AssetData(context, filePath, FileAccessFlags.READ) + Channels.newInputStream(assetData.readChannel) + } + + StorageScope.APP -> { + val fileData = FileData(filePath, FileAccessFlags.READ) + Channels.newInputStream(fileData.fileChannel) + } + StorageScope.SHARED -> { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + val mediaStoreData = MediaStoreData(context, filePath, FileAccessFlags.READ) + Channels.newInputStream(mediaStoreData.fileChannel) + } else { + null + } + } + + StorageScope.UNKNOWN -> null + } + } + + @Throws(java.lang.Exception::class, FileNotFoundException::class) fun generateDataAccess( storageScope: StorageScope, context: Context, @@ -61,6 +92,8 @@ internal abstract class DataAccess(private val filePath: String) { return when (storageScope) { StorageScope.APP -> FileData(filePath, accessFlag) + StorageScope.ASSETS -> AssetData(context, filePath, accessFlag) + StorageScope.SHARED -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { MediaStoreData(context, filePath, accessFlag) } else { @@ -74,7 +107,13 @@ internal abstract class DataAccess(private val filePath: String) { fun fileExists(storageScope: StorageScope, context: Context, path: String): Boolean { return when(storageScope) { StorageScope.APP -> FileData.fileExists(path) - StorageScope.SHARED -> MediaStoreData.fileExists(context, path) + StorageScope.ASSETS -> AssetData.fileExists(context, path) + StorageScope.SHARED -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + MediaStoreData.fileExists(context, path) + } else { + false + } + StorageScope.UNKNOWN -> false } } @@ -82,7 +121,13 @@ internal abstract class DataAccess(private val filePath: String) { fun fileLastModified(storageScope: StorageScope, context: Context, path: String): Long { return when(storageScope) { StorageScope.APP -> FileData.fileLastModified(path) - StorageScope.SHARED -> MediaStoreData.fileLastModified(context, path) + StorageScope.ASSETS -> AssetData.fileLastModified(path) + StorageScope.SHARED -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + MediaStoreData.fileLastModified(context, path) + } else { + 0L + } + StorageScope.UNKNOWN -> 0L } } @@ -90,7 +135,13 @@ internal abstract class DataAccess(private val filePath: String) { fun removeFile(storageScope: StorageScope, context: Context, path: String): Boolean { return when(storageScope) { StorageScope.APP -> FileData.delete(path) - StorageScope.SHARED -> MediaStoreData.delete(context, path) + StorageScope.ASSETS -> AssetData.delete(path) + StorageScope.SHARED -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + MediaStoreData.delete(context, path) + } else { + false + } + StorageScope.UNKNOWN -> false } } @@ -98,103 +149,120 @@ internal abstract class DataAccess(private val filePath: String) { fun renameFile(storageScope: StorageScope, context: Context, from: String, to: String): Boolean { return when(storageScope) { StorageScope.APP -> FileData.rename(from, to) - StorageScope.SHARED -> MediaStoreData.rename(context, from, to) + StorageScope.ASSETS -> AssetData.rename(from, to) + StorageScope.SHARED -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + MediaStoreData.rename(context, from, to) + } else { + false + } + StorageScope.UNKNOWN -> false } } } - protected abstract val fileChannel: FileChannel internal var endOfFile = false + abstract fun close() + abstract fun flush() + abstract fun seek(position: Long) + abstract fun resize(length: Long): Error + abstract fun position(): Long + abstract fun size(): Long + abstract fun read(buffer: ByteBuffer): Int + abstract fun write(buffer: ByteBuffer) - fun close() { - try { - fileChannel.close() - } catch (e: IOException) { - Log.w(TAG, "Exception when closing file $filePath.", e) - } + fun seekFromEnd(positionFromEnd: Long) { + val positionFromBeginning = max(0, size() - positionFromEnd) + seek(positionFromBeginning) } - fun flush() { - try { - fileChannel.force(false) - } catch (e: IOException) { - Log.w(TAG, "Exception when flushing file $filePath.", e) + abstract class FileChannelDataAccess(private val filePath: String) : DataAccess() { + internal abstract val fileChannel: FileChannel + + override fun close() { + try { + fileChannel.close() + } catch (e: IOException) { + Log.w(TAG, "Exception when closing file $filePath.", e) + } } - } - fun seek(position: Long) { - try { - fileChannel.position(position) - endOfFile = position >= fileChannel.size() - } catch (e: Exception) { - Log.w(TAG, "Exception when seeking file $filePath.", e) + override fun flush() { + try { + fileChannel.force(false) + } catch (e: IOException) { + Log.w(TAG, "Exception when flushing file $filePath.", e) + } } - } - fun seekFromEnd(positionFromEnd: Long) { - val positionFromBeginning = max(0, size() - positionFromEnd) - seek(positionFromBeginning) - } + override fun seek(position: Long) { + try { + fileChannel.position(position) + endOfFile = position >= fileChannel.size() + } catch (e: Exception) { + Log.w(TAG, "Exception when seeking file $filePath.", e) + } + } - fun resize(length: Long): Int { - return try { - fileChannel.truncate(length) - FileErrors.OK.nativeValue - } catch (e: NonWritableChannelException) { - FileErrors.FILE_CANT_OPEN.nativeValue - } catch (e: ClosedChannelException) { - FileErrors.FILE_CANT_OPEN.nativeValue - } catch (e: IllegalArgumentException) { - FileErrors.INVALID_PARAMETER.nativeValue - } catch (e: IOException) { - FileErrors.FAILED.nativeValue + override fun resize(length: Long): Error { + return try { + fileChannel.truncate(length) + Error.OK + } catch (e: NonWritableChannelException) { + Error.ERR_FILE_CANT_OPEN + } catch (e: ClosedChannelException) { + Error.ERR_FILE_CANT_OPEN + } catch (e: IllegalArgumentException) { + Error.ERR_INVALID_PARAMETER + } catch (e: IOException) { + Error.FAILED + } } - } - fun position(): Long { - return try { - fileChannel.position() + override fun position(): Long { + return try { + fileChannel.position() + } catch (e: IOException) { + Log.w( + TAG, + "Exception when retrieving position for file $filePath.", + e + ) + 0L + } + } + + override fun size() = try { + fileChannel.size() } catch (e: IOException) { - Log.w( - TAG, - "Exception when retrieving position for file $filePath.", - e - ) + Log.w(TAG, "Exception when retrieving size for file $filePath.", e) 0L } - } - - fun size() = try { - fileChannel.size() - } catch (e: IOException) { - Log.w(TAG, "Exception when retrieving size for file $filePath.", e) - 0L - } - fun read(buffer: ByteBuffer): Int { - return try { - val readBytes = fileChannel.read(buffer) - endOfFile = readBytes == -1 || (fileChannel.position() >= fileChannel.size()) - if (readBytes == -1) { + override fun read(buffer: ByteBuffer): Int { + return try { + val readBytes = fileChannel.read(buffer) + endOfFile = readBytes == -1 || (fileChannel.position() >= fileChannel.size()) + if (readBytes == -1) { + 0 + } else { + readBytes + } + } catch (e: IOException) { + Log.w(TAG, "Exception while reading from file $filePath.", e) 0 - } else { - readBytes } - } catch (e: IOException) { - Log.w(TAG, "Exception while reading from file $filePath.", e) - 0 } - } - fun write(buffer: ByteBuffer) { - try { - val writtenBytes = fileChannel.write(buffer) - if (writtenBytes > 0) { - endOfFile = false + override fun write(buffer: ByteBuffer) { + try { + val writtenBytes = fileChannel.write(buffer) + if (writtenBytes > 0) { + endOfFile = false + } + } catch (e: IOException) { + Log.w(TAG, "Exception while writing to file $filePath.", e) } - } catch (e: IOException) { - Log.w(TAG, "Exception while writing to file $filePath.", e) } } } diff --git a/platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessFlags.kt b/platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessFlags.kt index 38974af753..f81127e90a 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessFlags.kt +++ b/platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessFlags.kt @@ -76,7 +76,7 @@ internal enum class FileAccessFlags(val nativeValue: Int) { companion object { fun fromNativeModeFlags(modeFlag: Int): FileAccessFlags? { - for (flag in values()) { + for (flag in entries) { if (flag.nativeValue == modeFlag) { return flag } diff --git a/platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessHandler.kt b/platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessHandler.kt index 1d773467e8..dee7aebdc3 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessHandler.kt +++ b/platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessHandler.kt @@ -33,8 +33,11 @@ package org.godotengine.godot.io.file import android.content.Context import android.util.Log import android.util.SparseArray +import org.godotengine.godot.error.Error import org.godotengine.godot.io.StorageScope import java.io.FileNotFoundException +import java.io.InputStream +import java.lang.UnsupportedOperationException import java.nio.ByteBuffer /** @@ -45,8 +48,20 @@ class FileAccessHandler(val context: Context) { companion object { private val TAG = FileAccessHandler::class.java.simpleName - internal const val INVALID_FILE_ID = 0 + private const val INVALID_FILE_ID = 0 private const val STARTING_FILE_ID = 1 + private val FILE_OPEN_FAILED = Pair(Error.FAILED, INVALID_FILE_ID) + + internal fun getInputStream(context: Context, storageScopeIdentifier: StorageScope.Identifier, path: String?): InputStream? { + val storageScope = storageScopeIdentifier.identifyStorageScope(path) + return try { + path?.let { + DataAccess.getInputStream(storageScope, context, path) + } + } catch (e: Exception) { + null + } + } internal fun fileExists(context: Context, storageScopeIdentifier: StorageScope.Identifier, path: String?): Boolean { val storageScope = storageScopeIdentifier.identifyStorageScope(path) @@ -92,35 +107,55 @@ class FileAccessHandler(val context: Context) { } } - private val storageScopeIdentifier = StorageScope.Identifier(context) + internal val storageScopeIdentifier = StorageScope.Identifier(context) private val files = SparseArray<DataAccess>() private var lastFileId = STARTING_FILE_ID private fun hasFileId(fileId: Int) = files.indexOfKey(fileId) >= 0 + fun canAccess(filePath: String?): Boolean { + return storageScopeIdentifier.canAccess(filePath) + } + + /** + * Returns a positive (> 0) file id when the operation succeeds. + * Otherwise, returns a negative value of [Error]. + */ fun fileOpen(path: String?, modeFlags: Int): Int { - val accessFlag = FileAccessFlags.fromNativeModeFlags(modeFlags) ?: return INVALID_FILE_ID - return fileOpen(path, accessFlag) + val (fileError, fileId) = fileOpen(path, FileAccessFlags.fromNativeModeFlags(modeFlags)) + return if (fileError == Error.OK) { + fileId + } else { + // Return the negative of the [Error#toNativeValue()] value to differentiate from the + // positive file id. + -fileError.toNativeValue() + } } - internal fun fileOpen(path: String?, accessFlag: FileAccessFlags): Int { + internal fun fileOpen(path: String?, accessFlag: FileAccessFlags?): Pair<Error, Int> { + if (accessFlag == null) { + return FILE_OPEN_FAILED + } + val storageScope = storageScopeIdentifier.identifyStorageScope(path) if (storageScope == StorageScope.UNKNOWN) { - return INVALID_FILE_ID + return FILE_OPEN_FAILED } return try { path?.let { - val dataAccess = DataAccess.generateDataAccess(storageScope, context, it, accessFlag) ?: return INVALID_FILE_ID + val dataAccess = DataAccess.generateDataAccess(storageScope, context, it, accessFlag) ?: return FILE_OPEN_FAILED files.put(++lastFileId, dataAccess) - lastFileId - } ?: INVALID_FILE_ID + Pair(Error.OK, lastFileId) + } ?: FILE_OPEN_FAILED } catch (e: FileNotFoundException) { - FileErrors.FILE_NOT_FOUND.nativeValue + Pair(Error.ERR_FILE_NOT_FOUND, INVALID_FILE_ID) + } catch (e: UnsupportedOperationException) { + Pair(Error.ERR_UNAVAILABLE, INVALID_FILE_ID) } catch (e: Exception) { Log.w(TAG, "Error while opening $path", e) - INVALID_FILE_ID + FILE_OPEN_FAILED } } @@ -172,6 +207,10 @@ class FileAccessHandler(val context: Context) { files[fileId].flush() } + fun getInputStream(path: String?) = Companion.getInputStream(context, storageScopeIdentifier, path) + + fun renameFile(from: String, to: String) = Companion.renameFile(context, storageScopeIdentifier, from, to) + fun fileExists(path: String?) = Companion.fileExists(context, storageScopeIdentifier, path) fun fileLastModified(filepath: String?): Long { @@ -191,10 +230,10 @@ class FileAccessHandler(val context: Context) { fun fileResize(fileId: Int, length: Long): Int { if (!hasFileId(fileId)) { - return FileErrors.FAILED.nativeValue + return Error.FAILED.toNativeValue() } - return files[fileId].resize(length) + return files[fileId].resize(length).toNativeValue() } fun fileGetPosition(fileId: Int): Long { diff --git a/platform/android/java/lib/src/org/godotengine/godot/io/file/FileData.kt b/platform/android/java/lib/src/org/godotengine/godot/io/file/FileData.kt index d0b8a8dffa..873daada3c 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/io/file/FileData.kt +++ b/platform/android/java/lib/src/org/godotengine/godot/io/file/FileData.kt @@ -38,7 +38,7 @@ import java.nio.channels.FileChannel /** * Implementation of [DataAccess] which handles regular (not scoped) file access and interactions. */ -internal class FileData(filePath: String, accessFlag: FileAccessFlags) : DataAccess(filePath) { +internal class FileData(filePath: String, accessFlag: FileAccessFlags) : DataAccess.FileChannelDataAccess(filePath) { companion object { private val TAG = FileData::class.java.simpleName @@ -80,10 +80,16 @@ internal class FileData(filePath: String, accessFlag: FileAccessFlags) : DataAcc override val fileChannel: FileChannel init { - if (accessFlag == FileAccessFlags.WRITE) { - fileChannel = FileOutputStream(filePath, !accessFlag.shouldTruncate()).channel + fileChannel = if (accessFlag == FileAccessFlags.WRITE) { + // Create parent directory is necessary + val parentDir = File(filePath).parentFile + if (parentDir != null && !parentDir.exists()) { + parentDir.mkdirs() + } + + FileOutputStream(filePath, !accessFlag.shouldTruncate()).channel } else { - fileChannel = RandomAccessFile(filePath, accessFlag.getMode()).channel + RandomAccessFile(filePath, accessFlag.getMode()).channel } if (accessFlag.shouldTruncate()) { diff --git a/platform/android/java/lib/src/org/godotengine/godot/io/file/MediaStoreData.kt b/platform/android/java/lib/src/org/godotengine/godot/io/file/MediaStoreData.kt index 146fc04da4..97362e2542 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/io/file/MediaStoreData.kt +++ b/platform/android/java/lib/src/org/godotengine/godot/io/file/MediaStoreData.kt @@ -52,7 +52,7 @@ import java.nio.channels.FileChannel */ @RequiresApi(Build.VERSION_CODES.Q) internal class MediaStoreData(context: Context, filePath: String, accessFlag: FileAccessFlags) : - DataAccess(filePath) { + DataAccess.FileChannelDataAccess(filePath) { private data class DataItem( val id: Long, diff --git a/platform/android/java/lib/src/org/godotengine/godot/utils/BenchmarkUtils.kt b/platform/android/java/lib/src/org/godotengine/godot/utils/BenchmarkUtils.kt index d39f2309b8..738f27e877 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/utils/BenchmarkUtils.kt +++ b/platform/android/java/lib/src/org/godotengine/godot/utils/BenchmarkUtils.kt @@ -37,6 +37,7 @@ import android.os.SystemClock import android.os.Trace import android.util.Log import org.godotengine.godot.BuildConfig +import org.godotengine.godot.error.Error import org.godotengine.godot.io.file.FileAccessFlags import org.godotengine.godot.io.file.FileAccessHandler import org.json.JSONObject @@ -128,8 +129,8 @@ fun dumpBenchmark(fileAccessHandler: FileAccessHandler? = null, filepath: String Log.i(TAG, "BENCHMARK:\n$printOut") if (fileAccessHandler != null && !filepath.isNullOrBlank()) { - val fileId = fileAccessHandler.fileOpen(filepath, FileAccessFlags.WRITE) - if (fileId != FileAccessHandler.INVALID_FILE_ID) { + val (fileError, fileId) = fileAccessHandler.fileOpen(filepath, FileAccessFlags.WRITE) + if (fileError == Error.OK) { val jsonOutput = JSONObject(benchmarkTracker.toMap()).toString(4) fileAccessHandler.fileWrite(fileId, ByteBuffer.wrap(jsonOutput.toByteArray())) fileAccessHandler.fileClose(fileId) diff --git a/platform/android/java/lib/src/org/godotengine/godot/utils/ProcessPhoenix.java b/platform/android/java/lib/src/org/godotengine/godot/utils/ProcessPhoenix.java index b1bce45fbb..d9afdf90b1 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/utils/ProcessPhoenix.java +++ b/platform/android/java/lib/src/org/godotengine/godot/utils/ProcessPhoenix.java @@ -24,6 +24,7 @@ package org.godotengine.godot.utils; import android.app.Activity; import android.app.ActivityManager; +import android.app.ActivityOptions; import android.content.Context; import android.content.Intent; import android.os.Bundle; @@ -44,6 +45,9 @@ import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; */ public final class ProcessPhoenix extends Activity { private static final String KEY_RESTART_INTENTS = "phoenix_restart_intents"; + // -- GODOT start -- + private static final String KEY_RESTART_ACTIVITY_OPTIONS = "phoenix_restart_activity_options"; + // -- GODOT end -- private static final String KEY_MAIN_PROCESS_PID = "phoenix_main_process_pid"; /** @@ -56,12 +60,23 @@ public final class ProcessPhoenix extends Activity { triggerRebirth(context, getRestartIntent(context)); } + // -- GODOT start -- /** * Call to restart the application process using the specified intents. * <p> * Behavior of the current process after invoking this method is undefined. */ public static void triggerRebirth(Context context, Intent... nextIntents) { + triggerRebirth(context, null, nextIntents); + } + + /** + * Call to restart the application process using the specified intents launched with the given + * {@link ActivityOptions}. + * <p> + * Behavior of the current process after invoking this method is undefined. + */ + public static void triggerRebirth(Context context, Bundle activityOptions, Intent... nextIntents) { if (nextIntents.length < 1) { throw new IllegalArgumentException("intents cannot be empty"); } @@ -72,10 +87,12 @@ public final class ProcessPhoenix extends Activity { intent.addFlags(FLAG_ACTIVITY_NEW_TASK); // In case we are called with non-Activity context. intent.putParcelableArrayListExtra(KEY_RESTART_INTENTS, new ArrayList<>(Arrays.asList(nextIntents))); intent.putExtra(KEY_MAIN_PROCESS_PID, Process.myPid()); + if (activityOptions != null) { + intent.putExtra(KEY_RESTART_ACTIVITY_OPTIONS, activityOptions); + } context.startActivity(intent); } - // -- GODOT start -- /** * Finish the activity and kill its process */ @@ -112,9 +129,11 @@ public final class ProcessPhoenix extends Activity { super.onCreate(savedInstanceState); // -- GODOT start -- - ArrayList<Intent> intents = getIntent().getParcelableArrayListExtra(KEY_RESTART_INTENTS); - startActivities(intents.toArray(new Intent[intents.size()])); - forceQuit(this, getIntent().getIntExtra(KEY_MAIN_PROCESS_PID, -1)); + Intent launchIntent = getIntent(); + ArrayList<Intent> intents = launchIntent.getParcelableArrayListExtra(KEY_RESTART_INTENTS); + Bundle activityOptions = launchIntent.getBundleExtra(KEY_RESTART_ACTIVITY_OPTIONS); + startActivities(intents.toArray(new Intent[intents.size()]), activityOptions); + forceQuit(this, launchIntent.getIntExtra(KEY_MAIN_PROCESS_PID, -1)); // -- GODOT end -- } diff --git a/platform/android/java_godot_lib_jni.cpp b/platform/android/java_godot_lib_jni.cpp index a4a425f685..390677df22 100644 --- a/platform/android/java_godot_lib_jni.cpp +++ b/platform/android/java_godot_lib_jni.cpp @@ -51,7 +51,10 @@ #include "core/config/project_settings.h" #include "core/input/input.h" #include "main/main.h" + +#ifndef _3D_DISABLED #include "servers/xr_server.h" +#endif // _3D_DISABLED #ifdef TOOLS_ENABLED #include "editor/editor_settings.h" @@ -271,14 +274,16 @@ JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_step(JNIEnv *env, } if (step.get() == STEP_SHOW_LOGO) { - bool xr_enabled; + bool xr_enabled = false; +#ifndef _3D_DISABLED + // Unlike PCVR, there's no additional 2D screen onto which to render the boot logo, + // so we skip this step if xr is enabled. if (XRServer::get_xr_mode() == XRServer::XRMODE_DEFAULT) { xr_enabled = GLOBAL_GET("xr/shaders/enabled"); } else { xr_enabled = XRServer::get_xr_mode() == XRServer::XRMODE_ON; } - // Unlike PCVR, there's no additional 2D screen onto which to render the boot logo, - // so we skip this step if xr is enabled. +#endif // _3D_DISABLED if (!xr_enabled) { Main::setup_boot_logo(); } @@ -574,4 +579,9 @@ JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_shouldDispatchInp } return false; } + +JNIEXPORT jstring JNICALL Java_org_godotengine_godot_GodotLib_getProjectResourceDir(JNIEnv *env, jclass clazz) { + const String resource_dir = OS::get_singleton()->get_resource_dir(); + return env->NewStringUTF(resource_dir.utf8().get_data()); +} } diff --git a/platform/android/java_godot_lib_jni.h b/platform/android/java_godot_lib_jni.h index d027da31fa..2165ce264b 100644 --- a/platform/android/java_godot_lib_jni.h +++ b/platform/android/java_godot_lib_jni.h @@ -70,6 +70,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onNightModeChanged(JN JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onRendererResumed(JNIEnv *env, jclass clazz); JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onRendererPaused(JNIEnv *env, jclass clazz); JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_shouldDispatchInputToRenderThread(JNIEnv *env, jclass clazz); +JNIEXPORT jstring JNICALL Java_org_godotengine_godot_GodotLib_getProjectResourceDir(JNIEnv *env, jclass clazz); } #endif // JAVA_GODOT_LIB_JNI_H diff --git a/platform/android/java_godot_wrapper.cpp b/platform/android/java_godot_wrapper.cpp index 91bf7b48a6..d3b30e4589 100644 --- a/platform/android/java_godot_wrapper.cpp +++ b/platform/android/java_godot_wrapper.cpp @@ -84,6 +84,10 @@ GodotJavaWrapper::GodotJavaWrapper(JNIEnv *p_env, jobject p_activity, jobject p_ _dump_benchmark = p_env->GetMethodID(godot_class, "nativeDumpBenchmark", "(Ljava/lang/String;)V"); _get_gdextension_list_config_file = p_env->GetMethodID(godot_class, "getGDExtensionConfigFiles", "()[Ljava/lang/String;"); _has_feature = p_env->GetMethodID(godot_class, "hasFeature", "(Ljava/lang/String;)Z"); + _sign_apk = p_env->GetMethodID(godot_class, "nativeSignApk", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)I"); + _verify_apk = p_env->GetMethodID(godot_class, "nativeVerifyApk", "(Ljava/lang/String;)I"); + _enable_immersive_mode = p_env->GetMethodID(godot_class, "nativeEnableImmersiveMode", "(Z)V"); + _is_in_immersive_mode = p_env->GetMethodID(godot_class, "isInImmersiveMode", "()Z"); } GodotJavaWrapper::~GodotJavaWrapper() { @@ -424,3 +428,60 @@ bool GodotJavaWrapper::has_feature(const String &p_feature) const { return false; } } + +Error GodotJavaWrapper::sign_apk(const String &p_input_path, const String &p_output_path, const String &p_keystore_path, const String &p_keystore_user, const String &p_keystore_password) { + if (_sign_apk) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_NULL_V(env, ERR_UNCONFIGURED); + + jstring j_input_path = env->NewStringUTF(p_input_path.utf8().get_data()); + jstring j_output_path = env->NewStringUTF(p_output_path.utf8().get_data()); + jstring j_keystore_path = env->NewStringUTF(p_keystore_path.utf8().get_data()); + jstring j_keystore_user = env->NewStringUTF(p_keystore_user.utf8().get_data()); + jstring j_keystore_password = env->NewStringUTF(p_keystore_password.utf8().get_data()); + + int result = env->CallIntMethod(godot_instance, _sign_apk, j_input_path, j_output_path, j_keystore_path, j_keystore_user, j_keystore_password); + + env->DeleteLocalRef(j_input_path); + env->DeleteLocalRef(j_output_path); + env->DeleteLocalRef(j_keystore_path); + env->DeleteLocalRef(j_keystore_user); + env->DeleteLocalRef(j_keystore_password); + + return static_cast<Error>(result); + } else { + return ERR_UNCONFIGURED; + } +} + +Error GodotJavaWrapper::verify_apk(const String &p_apk_path) { + if (_verify_apk) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_NULL_V(env, ERR_UNCONFIGURED); + + jstring j_apk_path = env->NewStringUTF(p_apk_path.utf8().get_data()); + int result = env->CallIntMethod(godot_instance, _verify_apk, j_apk_path); + env->DeleteLocalRef(j_apk_path); + return static_cast<Error>(result); + } else { + return ERR_UNCONFIGURED; + } +} + +void GodotJavaWrapper::enable_immersive_mode(bool p_enabled) { + if (_enable_immersive_mode) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_NULL(env); + env->CallVoidMethod(godot_instance, _enable_immersive_mode, p_enabled); + } +} + +bool GodotJavaWrapper::is_in_immersive_mode() { + if (_is_in_immersive_mode) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_NULL_V(env, false); + return env->CallBooleanMethod(godot_instance, _is_in_immersive_mode); + } else { + return false; + } +} diff --git a/platform/android/java_godot_wrapper.h b/platform/android/java_godot_wrapper.h index 358cf3261d..51d7f98541 100644 --- a/platform/android/java_godot_wrapper.h +++ b/platform/android/java_godot_wrapper.h @@ -75,6 +75,10 @@ private: jmethodID _end_benchmark_measure = nullptr; jmethodID _dump_benchmark = nullptr; jmethodID _has_feature = nullptr; + jmethodID _sign_apk = nullptr; + jmethodID _verify_apk = nullptr; + jmethodID _enable_immersive_mode = nullptr; + jmethodID _is_in_immersive_mode = nullptr; public: GodotJavaWrapper(JNIEnv *p_env, jobject p_activity, jobject p_godot_instance); @@ -116,6 +120,13 @@ public: // Return true if the given feature is supported. bool has_feature(const String &p_feature) const; + + // Sign and verify apks + Error sign_apk(const String &p_input_path, const String &p_output_path, const String &p_keystore_path, const String &p_keystore_user, const String &p_keystore_password); + Error verify_apk(const String &p_apk_path); + + void enable_immersive_mode(bool p_enabled); + bool is_in_immersive_mode(); }; #endif // JAVA_GODOT_WRAPPER_H diff --git a/platform/android/os_android.cpp b/platform/android/os_android.cpp index 764959eef3..7b0d3a29e9 100644 --- a/platform/android/os_android.cpp +++ b/platform/android/os_android.cpp @@ -775,6 +775,16 @@ void OS_Android::benchmark_dump() { #endif } +#ifdef TOOLS_ENABLED +Error OS_Android::sign_apk(const String &p_input_path, const String &p_output_path, const String &p_keystore_path, const String &p_keystore_user, const String &p_keystore_password) { + return godot_java->sign_apk(p_input_path, p_output_path, p_keystore_path, p_keystore_user, p_keystore_password); +} + +Error OS_Android::verify_apk(const String &p_apk_path) { + return godot_java->verify_apk(p_apk_path); +} +#endif + bool OS_Android::_check_internal_feature_support(const String &p_feature) { if (p_feature == "macos" || p_feature == "web_ios" || p_feature == "web_macos" || p_feature == "windows") { return false; diff --git a/platform/android/os_android.h b/platform/android/os_android.h index b150ef4f61..fb3cdf0d4c 100644 --- a/platform/android/os_android.h +++ b/platform/android/os_android.h @@ -91,6 +91,11 @@ public: static const int DEFAULT_WINDOW_WIDTH = 800; static const int DEFAULT_WINDOW_HEIGHT = 600; +#ifdef TOOLS_ENABLED + Error sign_apk(const String &p_input_path, const String &p_output_path, const String &p_keystore_path, const String &p_keystore_user, const String &p_keystore_password); + Error verify_apk(const String &p_apk_path); +#endif + virtual void initialize_core() override; virtual void initialize() override; diff --git a/platform/ios/export/export_plugin.cpp b/platform/ios/export/export_plugin.cpp index e4b5392c4e..b99e825540 100644 --- a/platform/ios/export/export_plugin.cpp +++ b/platform/ios/export/export_plugin.cpp @@ -2017,11 +2017,11 @@ Error EditorExportPlatformIOS::_export_ios_plugins(const Ref<EditorExportPreset> return OK; } -Error EditorExportPlatformIOS::export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags) { +Error EditorExportPlatformIOS::export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, BitField<EditorExportPlatform::DebugFlags> p_flags) { return _export_project_helper(p_preset, p_debug, p_path, p_flags, false, false); } -Error EditorExportPlatformIOS::_export_project_helper(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags, bool p_simulator, bool p_oneclick) { +Error EditorExportPlatformIOS::_export_project_helper(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, BitField<EditorExportPlatform::DebugFlags> p_flags, bool p_simulator, bool p_oneclick) { ExportNotifier notifier(*this, p_preset, p_debug, p_path, p_flags); const String dest_dir = p_path.get_base_dir() + "/"; @@ -2983,7 +2983,7 @@ void EditorExportPlatformIOS::_update_preset_status() { } #endif -Error EditorExportPlatformIOS::run(const Ref<EditorExportPreset> &p_preset, int p_device, int p_debug_flags) { +Error EditorExportPlatformIOS::run(const Ref<EditorExportPreset> &p_preset, int p_device, BitField<EditorExportPlatform::DebugFlags> p_debug_flags) { #ifdef MACOS_ENABLED ERR_FAIL_INDEX_V(p_device, devices.size(), ERR_INVALID_PARAMETER); @@ -3029,11 +3029,11 @@ Error EditorExportPlatformIOS::run(const Ref<EditorExportPreset> &p_preset, int String host = EDITOR_GET("network/debug/remote_host"); int remote_port = (int)EDITOR_GET("network/debug/remote_port"); - if (p_debug_flags & DEBUG_FLAG_REMOTE_DEBUG_LOCALHOST) { + if (p_debug_flags.has_flag(DEBUG_FLAG_REMOTE_DEBUG_LOCALHOST)) { host = "localhost"; } - if (p_debug_flags & DEBUG_FLAG_DUMB_CLIENT) { + if (p_debug_flags.has_flag(DEBUG_FLAG_DUMB_CLIENT)) { int port = EDITOR_GET("filesystem/file_server/port"); String passwd = EDITOR_GET("filesystem/file_server/password"); cmd_args_list.push_back("--remote-fs"); @@ -3044,7 +3044,7 @@ Error EditorExportPlatformIOS::run(const Ref<EditorExportPreset> &p_preset, int } } - if (p_debug_flags & DEBUG_FLAG_REMOTE_DEBUG) { + if (p_debug_flags.has_flag(DEBUG_FLAG_REMOTE_DEBUG)) { cmd_args_list.push_back("--remote-debug"); cmd_args_list.push_back(get_debug_protocol() + host + ":" + String::num(remote_port)); @@ -3066,11 +3066,11 @@ Error EditorExportPlatformIOS::run(const Ref<EditorExportPreset> &p_preset, int } } - if (p_debug_flags & DEBUG_FLAG_VIEW_COLLISIONS) { + if (p_debug_flags.has_flag(DEBUG_FLAG_VIEW_COLLISIONS)) { cmd_args_list.push_back("--debug-collisions"); } - if (p_debug_flags & DEBUG_FLAG_VIEW_NAVIGATION) { + if (p_debug_flags.has_flag(DEBUG_FLAG_VIEW_NAVIGATION)) { cmd_args_list.push_back("--debug-navigation"); } diff --git a/platform/ios/export/export_plugin.h b/platform/ios/export/export_plugin.h index 1964906c27..db7c0553dd 100644 --- a/platform/ios/export/export_plugin.h +++ b/platform/ios/export/export_plugin.h @@ -146,7 +146,7 @@ class EditorExportPlatformIOS : public EditorExportPlatform { Error _export_additional_assets(const Ref<EditorExportPreset> &p_preset, const String &p_out_dir, const Vector<SharedObject> &p_libraries, Vector<IOSExportAsset> &r_exported_assets); Error _export_ios_plugins(const Ref<EditorExportPreset> &p_preset, IOSConfigData &p_config_data, const String &dest_dir, Vector<IOSExportAsset> &r_exported_assets, bool p_debug); - Error _export_project_helper(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags, bool p_simulator, bool p_oneclick); + Error _export_project_helper(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, BitField<EditorExportPlatform::DebugFlags> p_flags, bool p_simulator, bool p_oneclick); bool is_package_name_valid(const String &p_package, String *r_error = nullptr) const; @@ -169,7 +169,7 @@ public: virtual Ref<ImageTexture> get_option_icon(int p_index) const override; virtual String get_option_label(int p_index) const override; virtual String get_option_tooltip(int p_index) const override; - virtual Error run(const Ref<EditorExportPreset> &p_preset, int p_device, int p_debug_flags) override; + virtual Error run(const Ref<EditorExportPreset> &p_preset, int p_device, BitField<EditorExportPlatform::DebugFlags> p_debug_flags) override; virtual bool poll_export() override { bool dc = devices_changed.is_set(); @@ -202,7 +202,7 @@ public: return list; } - virtual Error export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags = 0) override; + virtual Error export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, BitField<EditorExportPlatform::DebugFlags> p_flags = 0) override; virtual bool has_valid_export_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates, bool p_debug = false) const override; virtual bool has_valid_project_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error) const override; diff --git a/platform/linuxbsd/export/export_plugin.cpp b/platform/linuxbsd/export/export_plugin.cpp index 0032b898d2..69ba742f72 100644 --- a/platform/linuxbsd/export/export_plugin.cpp +++ b/platform/linuxbsd/export/export_plugin.cpp @@ -60,7 +60,7 @@ Error EditorExportPlatformLinuxBSD::_export_debug_script(const Ref<EditorExportP return OK; } -Error EditorExportPlatformLinuxBSD::export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags) { +Error EditorExportPlatformLinuxBSD::export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, BitField<EditorExportPlatform::DebugFlags> p_flags) { String custom_debug = p_preset->get("custom_template/debug"); String custom_release = p_preset->get("custom_template/release"); String arch = p_preset->get("binary_format/architecture"); @@ -458,7 +458,7 @@ void EditorExportPlatformLinuxBSD::cleanup() { cleanup_commands.clear(); } -Error EditorExportPlatformLinuxBSD::run(const Ref<EditorExportPreset> &p_preset, int p_device, int p_debug_flags) { +Error EditorExportPlatformLinuxBSD::run(const Ref<EditorExportPreset> &p_preset, int p_device, BitField<EditorExportPlatform::DebugFlags> p_debug_flags) { cleanup(); if (p_device) { // Stop command, cleanup only. return OK; @@ -512,8 +512,7 @@ Error EditorExportPlatformLinuxBSD::run(const Ref<EditorExportPreset> &p_preset, String cmd_args; { - Vector<String> cmd_args_list; - gen_debug_flags(cmd_args_list, p_debug_flags); + Vector<String> cmd_args_list = gen_export_flags(p_debug_flags); for (int i = 0; i < cmd_args_list.size(); i++) { if (i != 0) { cmd_args += " "; @@ -522,7 +521,7 @@ Error EditorExportPlatformLinuxBSD::run(const Ref<EditorExportPreset> &p_preset, } } - const bool use_remote = (p_debug_flags & DEBUG_FLAG_REMOTE_DEBUG) || (p_debug_flags & DEBUG_FLAG_DUMB_CLIENT); + const bool use_remote = p_debug_flags.has_flag(DEBUG_FLAG_REMOTE_DEBUG) || p_debug_flags.has_flag(DEBUG_FLAG_DUMB_CLIENT); int dbg_port = EditorSettings::get_singleton()->get("network/debug/remote_port"); print_line("Creating temporary directory..."); diff --git a/platform/linuxbsd/export/export_plugin.h b/platform/linuxbsd/export/export_plugin.h index bbc55b82ce..1d9ef01d1a 100644 --- a/platform/linuxbsd/export/export_plugin.h +++ b/platform/linuxbsd/export/export_plugin.h @@ -76,7 +76,7 @@ public: virtual List<String> get_binary_extensions(const Ref<EditorExportPreset> &p_preset) const override; virtual bool get_export_option_visibility(const EditorExportPreset *p_preset, const String &p_option) const override; virtual bool has_valid_export_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates, bool p_debug = false) const override; - virtual Error export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags = 0) override; + virtual Error export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, BitField<EditorExportPlatform::DebugFlags> p_flags = 0) override; virtual String get_template_file_name(const String &p_target, const String &p_arch) const override; virtual Error fixup_embedded_pck(const String &p_path, int64_t p_embedded_start, int64_t p_embedded_size) override; virtual bool is_executable(const String &p_path) const override; @@ -87,7 +87,7 @@ public: virtual int get_options_count() const override; virtual String get_option_label(int p_index) const override; virtual String get_option_tooltip(int p_index) const override; - virtual Error run(const Ref<EditorExportPreset> &p_preset, int p_device, int p_debug_flags) override; + virtual Error run(const Ref<EditorExportPreset> &p_preset, int p_device, BitField<EditorExportPlatform::DebugFlags> p_debug_flags) override; virtual void cleanup() override; EditorExportPlatformLinuxBSD(); diff --git a/platform/linuxbsd/freedesktop_portal_desktop.cpp b/platform/linuxbsd/freedesktop_portal_desktop.cpp index 671da7fc2a..94a748e414 100644 --- a/platform/linuxbsd/freedesktop_portal_desktop.cpp +++ b/platform/linuxbsd/freedesktop_portal_desktop.cpp @@ -210,7 +210,15 @@ void FreeDesktopPortalDesktop::append_dbus_dict_filters(DBusMessageIter *p_iter, append_dbus_string(&struct_iter, p_filter_names[i]); dbus_message_iter_open_container(&struct_iter, DBUS_TYPE_ARRAY, "(us)", &array_iter); - const String &flt = p_filter_exts[i]; + const String &flt_orig = p_filter_exts[i]; + String flt; + for (int j = 0; j < flt_orig.length(); j++) { + if (is_unicode_letter(flt_orig[j])) { + flt += vformat("[%c%c]", String::char_lowercase(flt_orig[j]), String::char_uppercase(flt_orig[j])); + } else { + flt += flt_orig[j]; + } + } int filter_slice_count = flt.get_slice_count(","); for (int j = 0; j < filter_slice_count; j++) { dbus_message_iter_open_container(&array_iter, DBUS_TYPE_STRUCT, nullptr, &array_struct_iter); @@ -377,17 +385,26 @@ Error FreeDesktopPortalDesktop::file_dialog_show(DisplayServer::WindowID p_windo String flt = tokens[0].strip_edges(); if (!flt.is_empty()) { if (tokens.size() == 2) { - filter_exts.push_back(flt); + if (flt == "*.*") { + filter_exts.push_back("*"); + } else { + filter_exts.push_back(flt); + } filter_names.push_back(tokens[1]); } else { - filter_exts.push_back(flt); - filter_names.push_back(flt); + if (flt == "*.*") { + filter_exts.push_back("*"); + filter_names.push_back(RTR("All Files")); + } else { + filter_exts.push_back(flt); + filter_names.push_back(flt); + } } } } } if (filter_names.is_empty()) { - filter_exts.push_back("*.*"); + filter_exts.push_back("*"); filter_names.push_back(RTR("All Files")); } diff --git a/platform/linuxbsd/wayland/display_server_wayland.cpp b/platform/linuxbsd/wayland/display_server_wayland.cpp index 93096fcdcc..d1d83fe4ce 100644 --- a/platform/linuxbsd/wayland/display_server_wayland.cpp +++ b/platform/linuxbsd/wayland/display_server_wayland.cpp @@ -1429,6 +1429,7 @@ DisplayServerWayland::DisplayServerWayland(const String &p_rendering_driver, Win if (fallback) { WARN_PRINT("Your video card drivers seem not to support the required OpenGL version, switching to OpenGLES."); rendering_driver = "opengl3_es"; + OS::get_singleton()->set_current_rendering_driver_name(rendering_driver); } else { r_error = ERR_UNAVAILABLE; ERR_FAIL_MSG("Could not initialize OpenGL."); diff --git a/platform/linuxbsd/x11/display_server_x11.cpp b/platform/linuxbsd/x11/display_server_x11.cpp index 8a2f83be2d..e602963c54 100644 --- a/platform/linuxbsd/x11/display_server_x11.cpp +++ b/platform/linuxbsd/x11/display_server_x11.cpp @@ -6231,6 +6231,7 @@ DisplayServerX11::DisplayServerX11(const String &p_rendering_driver, WindowMode if (fallback) { WARN_PRINT("Your video card drivers seem not to support the required OpenGL version, switching to OpenGLES."); rendering_driver = "opengl3_es"; + OS::get_singleton()->set_current_rendering_driver_name(rendering_driver); } else { r_error = ERR_UNAVAILABLE; ERR_FAIL_MSG("Could not initialize OpenGL."); diff --git a/platform/macos/display_server_macos.mm b/platform/macos/display_server_macos.mm index 989a9dcf6c..52dc51bc96 100644 --- a/platform/macos/display_server_macos.mm +++ b/platform/macos/display_server_macos.mm @@ -3615,6 +3615,7 @@ DisplayServerMacOS::DisplayServerMacOS(const String &p_rendering_driver, WindowM WARN_PRINT("Your video card drivers seem not to support GLES3 / ANGLE or ANGLE dynamic libraries (libEGL.dylib and libGLESv2.dylib) are missing, switching to native OpenGL."); #endif rendering_driver = "opengl3"; + OS::get_singleton()->set_current_rendering_driver_name(rendering_driver); } else { r_error = ERR_UNAVAILABLE; ERR_FAIL_MSG("Could not initialize ANGLE OpenGL."); diff --git a/platform/macos/export/export_plugin.cpp b/platform/macos/export/export_plugin.cpp index 290b0082fc..8372600ae9 100644 --- a/platform/macos/export/export_plugin.cpp +++ b/platform/macos/export/export_plugin.cpp @@ -1505,7 +1505,7 @@ Error EditorExportPlatformMacOS::_export_debug_script(const Ref<EditorExportPres return OK; } -Error EditorExportPlatformMacOS::export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags) { +Error EditorExportPlatformMacOS::export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, BitField<EditorExportPlatform::DebugFlags> p_flags) { ExportNotifier notifier(*this, p_preset, p_debug, p_path, p_flags); const String base_dir = p_path.get_base_dir(); @@ -2511,7 +2511,7 @@ void EditorExportPlatformMacOS::cleanup() { cleanup_commands.clear(); } -Error EditorExportPlatformMacOS::run(const Ref<EditorExportPreset> &p_preset, int p_device, int p_debug_flags) { +Error EditorExportPlatformMacOS::run(const Ref<EditorExportPreset> &p_preset, int p_device, BitField<EditorExportPlatform::DebugFlags> p_debug_flags) { cleanup(); if (p_device) { // Stop command, cleanup only. return OK; @@ -2573,8 +2573,7 @@ Error EditorExportPlatformMacOS::run(const Ref<EditorExportPreset> &p_preset, in String cmd_args; { - Vector<String> cmd_args_list; - gen_debug_flags(cmd_args_list, p_debug_flags); + Vector<String> cmd_args_list = gen_export_flags(p_debug_flags); for (int i = 0; i < cmd_args_list.size(); i++) { if (i != 0) { cmd_args += " "; @@ -2583,7 +2582,7 @@ Error EditorExportPlatformMacOS::run(const Ref<EditorExportPreset> &p_preset, in } } - const bool use_remote = (p_debug_flags & DEBUG_FLAG_REMOTE_DEBUG) || (p_debug_flags & DEBUG_FLAG_DUMB_CLIENT); + const bool use_remote = p_debug_flags.has_flag(DEBUG_FLAG_REMOTE_DEBUG) || p_debug_flags.has_flag(DEBUG_FLAG_DUMB_CLIENT); int dbg_port = EditorSettings::get_singleton()->get("network/debug/remote_port"); print_line("Creating temporary directory..."); diff --git a/platform/macos/export/export_plugin.h b/platform/macos/export/export_plugin.h index 062a2e5f95..5457c687d3 100644 --- a/platform/macos/export/export_plugin.h +++ b/platform/macos/export/export_plugin.h @@ -147,7 +147,7 @@ public: virtual bool is_executable(const String &p_path) const override; virtual List<String> get_binary_extensions(const Ref<EditorExportPreset> &p_preset) const override; - virtual Error export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags = 0) override; + virtual Error export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, BitField<EditorExportPlatform::DebugFlags> p_flags = 0) override; virtual bool has_valid_export_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates, bool p_debug = false) const override; virtual bool has_valid_project_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error) const override; @@ -167,7 +167,7 @@ public: virtual int get_options_count() const override; virtual String get_option_label(int p_index) const override; virtual String get_option_tooltip(int p_index) const override; - virtual Error run(const Ref<EditorExportPreset> &p_preset, int p_device, int p_debug_flags) override; + virtual Error run(const Ref<EditorExportPreset> &p_preset, int p_device, BitField<EditorExportPlatform::DebugFlags> p_debug_flags) override; virtual void cleanup() override; EditorExportPlatformMacOS(); diff --git a/platform/web/export/export.cpp b/platform/web/export/export.cpp index 168310c078..306ec624a0 100644 --- a/platform/web/export/export.cpp +++ b/platform/web/export/export.cpp @@ -40,7 +40,6 @@ void register_web_exporter_types() { } void register_web_exporter() { -#ifndef ANDROID_ENABLED EDITOR_DEF("export/web/http_host", "localhost"); EDITOR_DEF("export/web/http_port", 8060); EDITOR_DEF("export/web/use_tls", false); @@ -49,7 +48,6 @@ void register_web_exporter() { EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::INT, "export/web/http_port", PROPERTY_HINT_RANGE, "1,65535,1")); EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::STRING, "export/web/tls_key", PROPERTY_HINT_GLOBAL_FILE, "*.key")); EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::STRING, "export/web/tls_certificate", PROPERTY_HINT_GLOBAL_FILE, "*.crt,*.pem")); -#endif Ref<EditorExportPlatformWeb> platform; platform.instantiate(); diff --git a/platform/web/export/export_plugin.cpp b/platform/web/export/export_plugin.cpp index d8c1b6033d..5faab74d7b 100644 --- a/platform/web/export/export_plugin.cpp +++ b/platform/web/export/export_plugin.cpp @@ -130,15 +130,14 @@ void EditorExportPlatformWeb::_replace_strings(const HashMap<String, String> &p_ } } -void EditorExportPlatformWeb::_fix_html(Vector<uint8_t> &p_html, const Ref<EditorExportPreset> &p_preset, const String &p_name, bool p_debug, int p_flags, const Vector<SharedObject> p_shared_objects, const Dictionary &p_file_sizes) { +void EditorExportPlatformWeb::_fix_html(Vector<uint8_t> &p_html, const Ref<EditorExportPreset> &p_preset, const String &p_name, bool p_debug, BitField<EditorExportPlatform::DebugFlags> p_flags, const Vector<SharedObject> p_shared_objects, const Dictionary &p_file_sizes) { // Engine.js config Dictionary config; Array libs; for (int i = 0; i < p_shared_objects.size(); i++) { libs.push_back(p_shared_objects[i].path.get_file()); } - Vector<String> flags; - gen_export_flags(flags, p_flags & (~DEBUG_FLAG_DUMB_CLIENT)); + Vector<String> flags = gen_export_flags(p_flags & (~DEBUG_FLAG_DUMB_CLIENT)); Array args; for (int i = 0; i < flags.size(); i++) { args.push_back(flags[i]); @@ -450,7 +449,7 @@ List<String> EditorExportPlatformWeb::get_binary_extensions(const Ref<EditorExpo return list; } -Error EditorExportPlatformWeb::export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags) { +Error EditorExportPlatformWeb::export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, BitField<EditorExportPlatform::DebugFlags> p_flags) { ExportNotifier notifier(*this, p_preset, p_debug, p_path, p_flags); const String custom_debug = p_preset->get("custom_template/debug"); @@ -744,7 +743,7 @@ String EditorExportPlatformWeb::get_option_tooltip(int p_index) const { return ""; } -Error EditorExportPlatformWeb::run(const Ref<EditorExportPreset> &p_preset, int p_option, int p_debug_flags) { +Error EditorExportPlatformWeb::run(const Ref<EditorExportPreset> &p_preset, int p_option, BitField<EditorExportPlatform::DebugFlags> p_debug_flags) { const uint16_t bind_port = EDITOR_GET("export/web/http_port"); // Resolve host if needed. const String bind_host = EDITOR_GET("export/web/http_host"); diff --git a/platform/web/export/export_plugin.h b/platform/web/export/export_plugin.h index 2f67d8107f..3c743e2e74 100644 --- a/platform/web/export/export_plugin.h +++ b/platform/web/export/export_plugin.h @@ -98,7 +98,7 @@ class EditorExportPlatformWeb : public EditorExportPlatform { Error _extract_template(const String &p_template, const String &p_dir, const String &p_name, bool pwa); void _replace_strings(const HashMap<String, String> &p_replaces, Vector<uint8_t> &r_template); - void _fix_html(Vector<uint8_t> &p_html, const Ref<EditorExportPreset> &p_preset, const String &p_name, bool p_debug, int p_flags, const Vector<SharedObject> p_shared_objects, const Dictionary &p_file_sizes); + void _fix_html(Vector<uint8_t> &p_html, const Ref<EditorExportPreset> &p_preset, const String &p_name, bool p_debug, BitField<EditorExportPlatform::DebugFlags> p_flags, const Vector<SharedObject> p_shared_objects, const Dictionary &p_file_sizes); Error _add_manifest_icon(const String &p_path, const String &p_icon, int p_size, Array &r_arr); Error _build_pwa(const Ref<EditorExportPreset> &p_preset, const String p_path, const Vector<SharedObject> &p_shared_objects); Error _write_or_error(const uint8_t *p_content, int p_len, String p_path); @@ -120,14 +120,14 @@ public: virtual bool has_valid_export_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates, bool p_debug = false) const override; virtual bool has_valid_project_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error) const override; virtual List<String> get_binary_extensions(const Ref<EditorExportPreset> &p_preset) const override; - virtual Error export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags = 0) override; + virtual Error export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, BitField<EditorExportPlatform::DebugFlags> p_flags = 0) override; virtual bool poll_export() override; virtual int get_options_count() const override; virtual String get_option_label(int p_index) const override; virtual String get_option_tooltip(int p_index) const override; virtual Ref<ImageTexture> get_option_icon(int p_index) const override; - virtual Error run(const Ref<EditorExportPreset> &p_preset, int p_option, int p_debug_flags) override; + virtual Error run(const Ref<EditorExportPreset> &p_preset, int p_option, BitField<EditorExportPlatform::DebugFlags> p_debug_flags) override; virtual Ref<Texture2D> get_run_icon() const override; virtual void get_platform_features(List<String> *r_features) const override { diff --git a/platform/web/http_client_web.cpp b/platform/web/http_client_web.cpp index ea9226a5a4..80257dc295 100644 --- a/platform/web/http_client_web.cpp +++ b/platform/web/http_client_web.cpp @@ -266,11 +266,11 @@ Error HTTPClientWeb::poll() { return OK; } -HTTPClient *HTTPClientWeb::_create_func() { - return memnew(HTTPClientWeb); +HTTPClient *HTTPClientWeb::_create_func(bool p_notify_postinitialize) { + return static_cast<HTTPClient *>(ClassDB::creator<HTTPClientWeb>(p_notify_postinitialize)); } -HTTPClient *(*HTTPClient::_create)() = HTTPClientWeb::_create_func; +HTTPClient *(*HTTPClient::_create)(bool p_notify_postinitialize) = HTTPClientWeb::_create_func; HTTPClientWeb::HTTPClientWeb() { } diff --git a/platform/web/http_client_web.h b/platform/web/http_client_web.h index 4d3c457a7d..f696c5a5b0 100644 --- a/platform/web/http_client_web.h +++ b/platform/web/http_client_web.h @@ -81,7 +81,7 @@ private: static void _parse_headers(int p_len, const char **p_headers, void *p_ref); public: - static HTTPClient *_create_func(); + static HTTPClient *_create_func(bool p_notify_postinitialize); Error request(Method p_method, const String &p_url, const Vector<String> &p_headers, const uint8_t *p_body, int p_body_size) override; diff --git a/platform/windows/console_wrapper_windows.cpp b/platform/windows/console_wrapper_windows.cpp index 133711a9ea..1ba09b236b 100644 --- a/platform/windows/console_wrapper_windows.cpp +++ b/platform/windows/console_wrapper_windows.cpp @@ -40,8 +40,8 @@ int main(int argc, char *argv[]) { // Get executable name. - WCHAR exe_name[MAX_PATH] = {}; - if (!GetModuleFileNameW(nullptr, exe_name, MAX_PATH)) { + WCHAR exe_name[32767] = {}; + if (!GetModuleFileNameW(nullptr, exe_name, 32767)) { wprintf(L"GetModuleFileName failed, error %d\n", GetLastError()); return -1; } diff --git a/platform/windows/crash_handler_windows_seh.cpp b/platform/windows/crash_handler_windows_seh.cpp index 2abe285d31..a6015092e8 100644 --- a/platform/windows/crash_handler_windows_seh.cpp +++ b/platform/windows/crash_handler_windows_seh.cpp @@ -118,7 +118,7 @@ DWORD CrashHandlerException(EXCEPTION_POINTERS *ep) { HANDLE process = GetCurrentProcess(); HANDLE hThread = GetCurrentThread(); DWORD offset_from_symbol = 0; - IMAGEHLP_LINE64 line = { 0 }; + IMAGEHLP_LINE64 line = {}; std::vector<module_data> modules; DWORD cbNeeded; std::vector<HMODULE> module_handles(1); diff --git a/platform/windows/detect.py b/platform/windows/detect.py index 11dd4548f1..92ac921cee 100644 --- a/platform/windows/detect.py +++ b/platform/windows/detect.py @@ -13,40 +13,33 @@ if TYPE_CHECKING: # To match other platforms STACK_SIZE = 8388608 +STACK_SIZE_SANITIZERS = 30 * 1024 * 1024 def get_name(): return "Windows" -def try_cmd(test, prefix, arch): +def try_cmd(test, prefix, arch, check_clang=False): + archs = ["x86_64", "x86_32", "arm64", "arm32"] if arch: + archs = [arch] + + for a in archs: try: out = subprocess.Popen( - get_mingw_bin_prefix(prefix, arch) + test, + get_mingw_bin_prefix(prefix, a) + test, shell=True, stderr=subprocess.PIPE, stdout=subprocess.PIPE, ) - out.communicate() + outs, errs = out.communicate() if out.returncode == 0: + if check_clang and not outs.startswith(b"clang"): + return False return True except Exception: pass - else: - for a in ["x86_64", "x86_32", "arm64", "arm32"]: - try: - out = subprocess.Popen( - get_mingw_bin_prefix(prefix, a) + test, - shell=True, - stderr=subprocess.PIPE, - stdout=subprocess.PIPE, - ) - out.communicate() - if out.returncode == 0: - return True - except Exception: - pass return False @@ -203,6 +196,7 @@ def get_opts(): BoolVariable("use_llvm", "Use the LLVM compiler", False), BoolVariable("use_static_cpp", "Link MinGW/MSVC C++ runtime libraries statically", True), BoolVariable("use_asan", "Use address sanitizer (ASAN)", False), + BoolVariable("use_ubsan", "Use LLVM compiler undefined behavior sanitizer (UBSAN)", False), BoolVariable("debug_crt", "Compile with MSVC's debug CRT (/MDd)", False), BoolVariable("incremental_link", "Use MSVC incremental linking. May increase or decrease build times.", False), BoolVariable("silence_msvc", "Silence MSVC's cl/link stdout bloat, redirecting any errors to stderr.", True), @@ -387,6 +381,15 @@ def configure_msvc(env: "SConsEnvironment", vcvars_msvc_config): ## Compile/link flags + if env["use_llvm"]: + env["CC"] = "clang-cl" + env["CXX"] = "clang-cl" + env["LINK"] = "lld-link" + env["AR"] = "llvm-lib" + + env.AppendUnique(CPPDEFINES=["R128_STDC_ONLY"]) + env.extra_suffix = ".llvm" + env.extra_suffix + env["MAXLINELENGTH"] = 8192 # Windows Vista and beyond, so always applicable. if env["silence_msvc"] and not env.GetOption("clean"): @@ -471,7 +474,6 @@ def configure_msvc(env: "SConsEnvironment", vcvars_msvc_config): env.AppendUnique(CCFLAGS=["/Gd", "/GR", "/nologo"]) env.AppendUnique(CCFLAGS=["/utf-8"]) # Force to use Unicode encoding. - env.AppendUnique(CXXFLAGS=["/TP"]) # assume all sources are C++ # Once it was thought that only debug builds would be too large, # but this has recently stopped being true. See the mingw function # for notes on why this shouldn't be enabled for gcc @@ -507,6 +509,7 @@ def configure_msvc(env: "SConsEnvironment", vcvars_msvc_config): if env["use_asan"]: env.extra_suffix += ".san" prebuilt_lib_extra_suffix = ".san" + env.AppendUnique(CPPDEFINES=["SANITIZERS_ENABLED"]) env.Append(CCFLAGS=["/fsanitize=address"]) env.Append(LINKFLAGS=["/INFERASANLIBS"]) @@ -595,6 +598,9 @@ def configure_msvc(env: "SConsEnvironment", vcvars_msvc_config): if env["target"] in ["editor", "template_debug"]: LIBS += ["psapi", "dbghelp"] + if env["use_llvm"]: + LIBS += [f"clang_rt.builtins-{env['arch']}"] + env.Append(LINKFLAGS=[p + env["LIBSUFFIX"] for p in LIBS]) if vcvars_msvc_config: @@ -610,14 +616,22 @@ def configure_msvc(env: "SConsEnvironment", vcvars_msvc_config): if env["lto"] != "none": if env["lto"] == "thin": - print_error("ThinLTO is only compatible with LLVM, use `use_llvm=yes` or `lto=full`.") - sys.exit(255) - env.AppendUnique(CCFLAGS=["/GL"]) - env.AppendUnique(ARFLAGS=["/LTCG"]) - if env["progress"]: - env.AppendUnique(LINKFLAGS=["/LTCG:STATUS"]) + if not env["use_llvm"]: + print("ThinLTO is only compatible with LLVM, use `use_llvm=yes` or `lto=full`.") + sys.exit(255) + + env.Append(CCFLAGS=["-flto=thin"]) + env.Append(LINKFLAGS=["-flto=thin"]) + elif env["use_llvm"]: + env.Append(CCFLAGS=["-flto"]) + env.Append(LINKFLAGS=["-flto"]) else: - env.AppendUnique(LINKFLAGS=["/LTCG"]) + env.AppendUnique(CCFLAGS=["/GL"]) + env.AppendUnique(ARFLAGS=["/LTCG"]) + if env["progress"]: + env.AppendUnique(LINKFLAGS=["/LTCG:STATUS"]) + else: + env.AppendUnique(LINKFLAGS=["/LTCG"]) if vcvars_msvc_config: env.Prepend(CPPPATH=[p for p in str(os.getenv("INCLUDE")).split(";")]) @@ -628,7 +642,66 @@ def configure_msvc(env: "SConsEnvironment", vcvars_msvc_config): env["BUILDERS"]["Program"] = methods.precious_program env.Append(LINKFLAGS=["/NATVIS:platform\\windows\\godot.natvis"]) - env.AppendUnique(LINKFLAGS=["/STACK:" + str(STACK_SIZE)]) + + if env["use_asan"]: + env.AppendUnique(LINKFLAGS=["/STACK:" + str(STACK_SIZE_SANITIZERS)]) + else: + env.AppendUnique(LINKFLAGS=["/STACK:" + str(STACK_SIZE)]) + + +def get_ar_version(env): + ret = { + "major": -1, + "minor": -1, + "patch": -1, + "is_llvm": False, + } + try: + output = ( + subprocess.check_output([env.subst(env["AR"]), "--version"], shell=(os.name == "nt")) + .strip() + .decode("utf-8") + ) + except (subprocess.CalledProcessError, OSError): + print_warning("Couldn't check version of `ar`.") + return ret + + match = re.search(r"GNU ar \(GNU Binutils\) (\d+)\.(\d+)(:?\.(\d+))?", output) + if match: + ret["major"] = int(match[1]) + ret["minor"] = int(match[2]) + if match[3]: + ret["patch"] = int(match[3]) + else: + ret["patch"] = 0 + return ret + + match = re.search(r"LLVM version (\d+)\.(\d+)\.(\d+)", output) + if match: + ret["major"] = int(match[1]) + ret["minor"] = int(match[2]) + ret["patch"] = int(match[3]) + ret["is_llvm"] = True + return ret + + print_warning("Couldn't parse version of `ar`.") + return ret + + +def get_is_ar_thin_supported(env): + """Check whether `ar --thin` is supported. It is only supported since Binutils 2.38 or LLVM 14.""" + ar_version = get_ar_version(env) + if ar_version["major"] == -1: + return False + + if ar_version["is_llvm"]: + return ar_version["major"] >= 14 + + if ar_version["major"] == 2: + return ar_version["minor"] >= 38 + + print_warning("Unknown Binutils `ar` version.") + return False def configure_mingw(env: "SConsEnvironment"): @@ -644,6 +717,10 @@ def configure_mingw(env: "SConsEnvironment"): if env["use_llvm"] and not try_cmd("clang --version", env["mingw_prefix"], env["arch"]): env["use_llvm"] = False + if not env["use_llvm"] and try_cmd("gcc --version", env["mingw_prefix"], env["arch"], True): + print("Detected GCC to be a wrapper for Clang.") + env["use_llvm"] = True + # TODO: Re-evaluate the need for this / streamline with common config. if env["target"] == "template_release": if env["arch"] != "arm64": @@ -721,7 +798,10 @@ def configure_mingw(env: "SConsEnvironment"): env.Append(CCFLAGS=["-flto"]) env.Append(LINKFLAGS=["-flto"]) - env.Append(LINKFLAGS=["-Wl,--stack," + str(STACK_SIZE)]) + if env["use_asan"]: + env.Append(LINKFLAGS=["-Wl,--stack," + str(STACK_SIZE_SANITIZERS)]) + else: + env.Append(LINKFLAGS=["-Wl,--stack," + str(STACK_SIZE)]) ## Compile flags @@ -732,6 +812,33 @@ def configure_mingw(env: "SConsEnvironment"): if not env["use_llvm"]: env.Append(CCFLAGS=["-mwindows"]) + if env["use_asan"] or env["use_ubsan"]: + if not env["use_llvm"]: + print("GCC does not support sanitizers on Windows.") + sys.exit(255) + if env["arch"] not in ["x86_32", "x86_64"]: + print("Sanitizers are only supported for x86_32 and x86_64.") + sys.exit(255) + + env.extra_suffix += ".san" + env.AppendUnique(CPPDEFINES=["SANITIZERS_ENABLED"]) + san_flags = [] + if env["use_asan"]: + san_flags.append("-fsanitize=address") + if env["use_ubsan"]: + san_flags.append("-fsanitize=undefined") + # Disable the vptr check since it gets triggered on any COM interface calls. + san_flags.append("-fno-sanitize=vptr") + env.Append(CFLAGS=san_flags) + env.Append(CCFLAGS=san_flags) + env.Append(LINKFLAGS=san_flags) + + if env["use_llvm"] and os.name == "nt" and methods._colorize: + env.Append(CCFLAGS=["$(-fansi-escape-codes$)", "$(-fcolor-diagnostics$)"]) + + if get_is_ar_thin_supported(env): + env.Append(ARFLAGS=["--thin"]) + env.Append(CPPDEFINES=["WINDOWS_ENABLED", "WASAPI_ENABLED", "WINMIDI_ENABLED"]) env.Append( CPPDEFINES=[ diff --git a/platform/windows/display_server_windows.cpp b/platform/windows/display_server_windows.cpp index 6fa3f2c9d6..602ca5f52e 100644 --- a/platform/windows/display_server_windows.cpp +++ b/platform/windows/display_server_windows.cpp @@ -133,9 +133,17 @@ String DisplayServerWindows::get_name() const { } void DisplayServerWindows::_set_mouse_mode_impl(MouseMode p_mode) { + if (p_mode == MOUSE_MODE_HIDDEN || p_mode == MOUSE_MODE_CAPTURED || p_mode == MOUSE_MODE_CONFINED_HIDDEN) { + // Hide cursor before moving. + if (hCursor == nullptr) { + hCursor = SetCursor(nullptr); + } else { + SetCursor(nullptr); + } + } + if (windows.has(MAIN_WINDOW_ID) && (p_mode == MOUSE_MODE_CAPTURED || p_mode == MOUSE_MODE_CONFINED || p_mode == MOUSE_MODE_CONFINED_HIDDEN)) { // Mouse is grabbed (captured or confined). - WindowID window_id = _get_focused_window_or_popup(); if (!windows.has(window_id)) { window_id = MAIN_WINDOW_ID; @@ -165,13 +173,8 @@ void DisplayServerWindows::_set_mouse_mode_impl(MouseMode p_mode) { _register_raw_input_devices(INVALID_WINDOW_ID); } - if (p_mode == MOUSE_MODE_HIDDEN || p_mode == MOUSE_MODE_CAPTURED || p_mode == MOUSE_MODE_CONFINED_HIDDEN) { - if (hCursor == nullptr) { - hCursor = SetCursor(nullptr); - } else { - SetCursor(nullptr); - } - } else { + if (p_mode == MOUSE_MODE_VISIBLE || p_mode == MOUSE_MODE_CONFINED) { + // Show cursor. CursorShape c = cursor_shape; cursor_shape = CURSOR_MAX; cursor_set_shape(c); @@ -314,7 +317,7 @@ public: if (!lpw_path) { return S_FALSE; } - String path = String::utf16((const char16_t *)lpw_path).simplify_path(); + String path = String::utf16((const char16_t *)lpw_path).replace("\\", "/").trim_prefix(R"(\\?\)").simplify_path(); if (!path.begins_with(root.simplify_path())) { return S_FALSE; } @@ -539,7 +542,26 @@ void DisplayServerWindows::_thread_fd_monitor(void *p_ud) { pfd->SetOptions(flags | FOS_FORCEFILESYSTEM); pfd->SetTitle((LPCWSTR)fd->title.utf16().ptr()); - String dir = fd->current_directory.replace("/", "\\"); + String dir = ProjectSettings::get_singleton()->globalize_path(fd->current_directory); + if (dir == ".") { + dir = OS::get_singleton()->get_executable_path().get_base_dir(); + } + if (dir.is_relative_path() || dir == ".") { + Char16String current_dir_name; + size_t str_len = GetCurrentDirectoryW(0, nullptr); + current_dir_name.resize(str_len + 1); + GetCurrentDirectoryW(current_dir_name.size(), (LPWSTR)current_dir_name.ptrw()); + if (dir == ".") { + dir = String::utf16((const char16_t *)current_dir_name.get_data()).trim_prefix(R"(\\?\)").replace("\\", "/"); + } else { + dir = String::utf16((const char16_t *)current_dir_name.get_data()).trim_prefix(R"(\\?\)").replace("\\", "/").path_join(dir); + } + } + dir = dir.simplify_path(); + dir = dir.replace("/", "\\"); + if (!dir.is_network_share_path() && !dir.begins_with(R"(\\?\)")) { + dir = R"(\\?\)" + dir; + } IShellItem *shellitem = nullptr; hr = SHCreateItemFromParsingName((LPCWSTR)dir.utf16().ptr(), nullptr, IID_IShellItem, (void **)&shellitem); @@ -582,7 +604,7 @@ void DisplayServerWindows::_thread_fd_monitor(void *p_ud) { PWSTR file_path = nullptr; hr = result->GetDisplayName(SIGDN_FILESYSPATH, &file_path); if (SUCCEEDED(hr)) { - file_names.push_back(String::utf16((const char16_t *)file_path)); + file_names.push_back(String::utf16((const char16_t *)file_path).replace("\\", "/").trim_prefix(R"(\\?\)")); CoTaskMemFree(file_path); } result->Release(); @@ -596,7 +618,7 @@ void DisplayServerWindows::_thread_fd_monitor(void *p_ud) { PWSTR file_path = nullptr; hr = result->GetDisplayName(SIGDN_FILESYSPATH, &file_path); if (SUCCEEDED(hr)) { - file_names.push_back(String::utf16((const char16_t *)file_path)); + file_names.push_back(String::utf16((const char16_t *)file_path).replace("\\", "/").trim_prefix(R"(\\?\)")); CoTaskMemFree(file_path); } result->Release(); @@ -6128,6 +6150,7 @@ DisplayServerWindows::DisplayServerWindows(const String &p_rendering_driver, Win if (rendering_context->initialize() == OK) { WARN_PRINT("Your video card drivers seem not to support Direct3D 12, switching to Vulkan."); rendering_driver = "vulkan"; + OS::get_singleton()->set_current_rendering_driver_name(rendering_driver); failed = false; } } @@ -6141,6 +6164,7 @@ DisplayServerWindows::DisplayServerWindows(const String &p_rendering_driver, Win if (rendering_context->initialize() == OK) { WARN_PRINT("Your video card drivers seem not to support Vulkan, switching to Direct3D 12."); rendering_driver = "d3d12"; + OS::get_singleton()->set_current_rendering_driver_name(rendering_driver); failed = false; } } @@ -6219,6 +6243,7 @@ DisplayServerWindows::DisplayServerWindows(const String &p_rendering_driver, Win } } rendering_driver = "opengl3_angle"; + OS::get_singleton()->set_current_rendering_driver_name(rendering_driver); } } diff --git a/platform/windows/export/export_plugin.cpp b/platform/windows/export/export_plugin.cpp index b465bd4ecd..8d3f4bb269 100644 --- a/platform/windows/export/export_plugin.cpp +++ b/platform/windows/export/export_plugin.cpp @@ -167,7 +167,7 @@ Error EditorExportPlatformWindows::sign_shared_object(const Ref<EditorExportPres } } -Error EditorExportPlatformWindows::modify_template(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags) { +Error EditorExportPlatformWindows::modify_template(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, BitField<EditorExportPlatform::DebugFlags> p_flags) { if (p_preset->get("application/modify_resources")) { _rcedit_add_data(p_preset, p_path, false); String wrapper_path = p_path.get_basename() + ".console.exe"; @@ -178,7 +178,7 @@ Error EditorExportPlatformWindows::modify_template(const Ref<EditorExportPreset> return OK; } -Error EditorExportPlatformWindows::export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags) { +Error EditorExportPlatformWindows::export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, BitField<EditorExportPlatform::DebugFlags> p_flags) { String custom_debug = p_preset->get("custom_template/debug"); String custom_release = p_preset->get("custom_template/release"); String arch = p_preset->get("binary_format/architecture"); @@ -996,7 +996,7 @@ void EditorExportPlatformWindows::cleanup() { cleanup_commands.clear(); } -Error EditorExportPlatformWindows::run(const Ref<EditorExportPreset> &p_preset, int p_device, int p_debug_flags) { +Error EditorExportPlatformWindows::run(const Ref<EditorExportPreset> &p_preset, int p_device, BitField<EditorExportPlatform::DebugFlags> p_debug_flags) { cleanup(); if (p_device) { // Stop command, cleanup only. return OK; @@ -1050,8 +1050,7 @@ Error EditorExportPlatformWindows::run(const Ref<EditorExportPreset> &p_preset, String cmd_args; { - Vector<String> cmd_args_list; - gen_debug_flags(cmd_args_list, p_debug_flags); + Vector<String> cmd_args_list = gen_export_flags(p_debug_flags); for (int i = 0; i < cmd_args_list.size(); i++) { if (i != 0) { cmd_args += " "; @@ -1060,7 +1059,7 @@ Error EditorExportPlatformWindows::run(const Ref<EditorExportPreset> &p_preset, } } - const bool use_remote = (p_debug_flags & DEBUG_FLAG_REMOTE_DEBUG) || (p_debug_flags & DEBUG_FLAG_DUMB_CLIENT); + const bool use_remote = p_debug_flags.has_flag(DEBUG_FLAG_REMOTE_DEBUG) || p_debug_flags.has_flag(DEBUG_FLAG_DUMB_CLIENT); int dbg_port = EditorSettings::get_singleton()->get("network/debug/remote_port"); print_line("Creating temporary directory..."); diff --git a/platform/windows/export/export_plugin.h b/platform/windows/export/export_plugin.h index 6ccb4a15a7..e86aac83d4 100644 --- a/platform/windows/export/export_plugin.h +++ b/platform/windows/export/export_plugin.h @@ -76,8 +76,8 @@ class EditorExportPlatformWindows : public EditorExportPlatformPC { String _get_exe_arch(const String &p_path) const; public: - virtual Error export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags = 0) override; - virtual Error modify_template(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags) override; + virtual Error export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, BitField<EditorExportPlatform::DebugFlags> p_flags = 0) override; + virtual Error modify_template(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, BitField<EditorExportPlatform::DebugFlags> p_flags) override; virtual Error sign_shared_object(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path) override; virtual List<String> get_binary_extensions(const Ref<EditorExportPreset> &p_preset) const override; virtual void get_export_options(List<ExportOption> *r_options) const override; @@ -95,7 +95,7 @@ public: virtual int get_options_count() const override; virtual String get_option_label(int p_index) const override; virtual String get_option_tooltip(int p_index) const override; - virtual Error run(const Ref<EditorExportPreset> &p_preset, int p_device, int p_debug_flags) override; + virtual Error run(const Ref<EditorExportPreset> &p_preset, int p_device, BitField<EditorExportPlatform::DebugFlags> p_debug_flags) override; virtual void cleanup() override; EditorExportPlatformWindows(); diff --git a/platform/windows/godot_res_wrap.rc b/platform/windows/godot_res_wrap.rc index 27ad26cbc5..61e6100497 100644 --- a/platform/windows/godot_res_wrap.rc +++ b/platform/windows/godot_res_wrap.rc @@ -1,6 +1,11 @@ #include "core/version.h" +#ifndef RT_MANIFEST +#define RT_MANIFEST 24 +#endif + GODOT_ICON ICON platform/windows/godot_console.ico +1 RT_MANIFEST "godot.manifest" 1 VERSIONINFO FILEVERSION VERSION_MAJOR,VERSION_MINOR,VERSION_PATCH,0 diff --git a/platform/windows/os_windows.cpp b/platform/windows/os_windows.cpp index 40b265785f..47836788e1 100644 --- a/platform/windows/os_windows.cpp +++ b/platform/windows/os_windows.cpp @@ -90,6 +90,23 @@ __declspec(dllexport) int AmdPowerXpressRequestHighPerformance = 1; #define GetProcAddress (void *)GetProcAddress #endif +static String fix_path(const String &p_path) { + String path = p_path; + if (p_path.is_relative_path()) { + Char16String current_dir_name; + size_t str_len = GetCurrentDirectoryW(0, nullptr); + current_dir_name.resize(str_len + 1); + GetCurrentDirectoryW(current_dir_name.size(), (LPWSTR)current_dir_name.ptrw()); + path = String::utf16((const char16_t *)current_dir_name.get_data()).trim_prefix(R"(\\?\)").replace("\\", "/").path_join(path); + } + path = path.simplify_path(); + path = path.replace("/", "\\"); + if (!path.is_network_share_path() && !path.begins_with(R"(\\?\)")) { + path = R"(\\?\)" + path; + } + return path; +} + static String format_error_message(DWORD id) { LPWSTR messageBuffer = nullptr; size_t size = FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, @@ -166,15 +183,9 @@ void OS_Windows::initialize_debugging() { static void _error_handler(void *p_self, const char *p_func, const char *p_file, int p_line, const char *p_error, const char *p_errorexp, bool p_editor_notify, ErrorHandlerType p_type) { String err_str; if (p_errorexp && p_errorexp[0]) { - err_str = String::utf8(p_errorexp); + err_str = String::utf8(p_errorexp) + "\n"; } else { - err_str = String::utf8(p_file) + ":" + itos(p_line) + " - " + String::utf8(p_error); - } - - if (p_editor_notify) { - err_str += " (User)\n"; - } else { - err_str += "\n"; + err_str = String::utf8(p_file) + ":" + itos(p_line) + " - " + String::utf8(p_error) + "\n"; } OutputDebugStringW((LPCWSTR)err_str.utf16().ptr()); @@ -306,7 +317,7 @@ Error OS_Windows::get_entropy(uint8_t *r_buffer, int p_bytes) { } #ifdef DEBUG_ENABLED -void debug_dynamic_library_check_dependencies(const String &p_root_path, const String &p_path, HashSet<String> &r_checked, HashSet<String> &r_missing) { +void debug_dynamic_library_check_dependencies(const String &p_path, HashSet<String> &r_checked, HashSet<String> &r_missing) { if (r_checked.has(p_path)) { return; } @@ -348,15 +359,15 @@ void debug_dynamic_library_check_dependencies(const String &p_root_path, const S const IMAGE_IMPORT_DESCRIPTOR *import_desc = (const IMAGE_IMPORT_DESCRIPTOR *)ImageDirectoryEntryToData((HMODULE)loaded_image.MappedAddress, false, IMAGE_DIRECTORY_ENTRY_IMPORT, &size); if (import_desc) { for (; import_desc->Name && import_desc->FirstThunk; import_desc++) { - char16_t full_name_wc[MAX_PATH]; + char16_t full_name_wc[32767]; const char *name_cs = (const char *)ImageRvaToVa(loaded_image.FileHeader, loaded_image.MappedAddress, import_desc->Name, nullptr); String name = String(name_cs); if (name.begins_with("api-ms-win-")) { r_checked.insert(name); - } else if (SearchPathW(nullptr, (LPCWSTR)name.utf16().get_data(), nullptr, MAX_PATH, (LPWSTR)full_name_wc, nullptr)) { - debug_dynamic_library_check_dependencies(p_root_path, String::utf16(full_name_wc), r_checked, r_missing); - } else if (SearchPathW((LPCWSTR)(p_path.get_base_dir().utf16().get_data()), (LPCWSTR)name.utf16().get_data(), nullptr, MAX_PATH, (LPWSTR)full_name_wc, nullptr)) { - debug_dynamic_library_check_dependencies(p_root_path, String::utf16(full_name_wc), r_checked, r_missing); + } else if (SearchPathW(nullptr, (LPCWSTR)name.utf16().get_data(), nullptr, 32767, (LPWSTR)full_name_wc, nullptr)) { + debug_dynamic_library_check_dependencies(String::utf16(full_name_wc), r_checked, r_missing); + } else if (SearchPathW((LPCWSTR)(p_path.get_base_dir().utf16().get_data()), (LPCWSTR)name.utf16().get_data(), nullptr, 32767, (LPWSTR)full_name_wc, nullptr)) { + debug_dynamic_library_check_dependencies(String::utf16(full_name_wc), r_checked, r_missing); } else { r_missing.insert(name); } @@ -373,7 +384,7 @@ void debug_dynamic_library_check_dependencies(const String &p_root_path, const S #endif Error OS_Windows::open_dynamic_library(const String &p_path, void *&p_library_handle, GDExtensionData *p_data) { - String path = p_path.replace("/", "\\"); + String path = p_path; if (!FileAccess::exists(path)) { //this code exists so gdextension can load .dll files from within the executable path @@ -419,11 +430,13 @@ Error OS_Windows::open_dynamic_library(const String &p_path, void *&p_library_ha bool has_dll_directory_api = ((add_dll_directory != nullptr) && (remove_dll_directory != nullptr)); DLL_DIRECTORY_COOKIE cookie = nullptr; + String dll_path = fix_path(load_path); + String dll_dir = fix_path(ProjectSettings::get_singleton()->globalize_path(load_path.get_base_dir())); if (p_data != nullptr && p_data->also_set_library_path && has_dll_directory_api) { - cookie = add_dll_directory((LPCWSTR)(load_path.get_base_dir().utf16().get_data())); + cookie = add_dll_directory((LPCWSTR)(dll_dir.utf16().get_data())); } - p_library_handle = (void *)LoadLibraryExW((LPCWSTR)(load_path.utf16().get_data()), nullptr, (p_data != nullptr && p_data->also_set_library_path && has_dll_directory_api) ? LOAD_LIBRARY_SEARCH_DEFAULT_DIRS : 0); + p_library_handle = (void *)LoadLibraryExW((LPCWSTR)(dll_path.utf16().get_data()), nullptr, (p_data != nullptr && p_data->also_set_library_path && has_dll_directory_api) ? LOAD_LIBRARY_SEARCH_DEFAULT_DIRS : 0); if (!p_library_handle) { if (p_data != nullptr && p_data->generate_temp_files) { DirAccess::remove_absolute(load_path); @@ -434,7 +447,7 @@ Error OS_Windows::open_dynamic_library(const String &p_path, void *&p_library_ha HashSet<String> checked_libs; HashSet<String> missing_libs; - debug_dynamic_library_check_dependencies(load_path, load_path, checked_libs, missing_libs); + debug_dynamic_library_check_dependencies(dll_path, checked_libs, missing_libs); if (!missing_libs.is_empty()) { String missing; for (const String &E : missing_libs) { @@ -622,6 +635,72 @@ Vector<String> OS_Windows::get_video_adapter_driver_info() const { return info; } +bool OS_Windows::get_user_prefers_integrated_gpu() const { + // On Windows 10, the preferred GPU configured in Windows Settings is + // stored in the registry under the key + // `HKEY_CURRENT_USER\SOFTWARE\Microsoft\DirectX\UserGpuPreferences` + // with the name being the app ID or EXE path. The value is in the form of + // `GpuPreference=1;`, with the value being 1 for integrated GPU and 2 + // for discrete GPU. On Windows 11, there may be more flags, separated + // by semicolons. + + // If this is a packaged app, use the "application user model ID". + // Otherwise, use the EXE path. + WCHAR value_name[32768]; + bool is_packaged = false; + { + HMODULE kernel32 = GetModuleHandleW(L"kernel32.dll"); + if (kernel32) { + using GetCurrentApplicationUserModelIdPtr = LONG(WINAPI *)(UINT32 * length, PWSTR id); + GetCurrentApplicationUserModelIdPtr GetCurrentApplicationUserModelId = (GetCurrentApplicationUserModelIdPtr)GetProcAddress(kernel32, "GetCurrentApplicationUserModelId"); + + if (GetCurrentApplicationUserModelId) { + UINT32 length = sizeof(value_name) / sizeof(value_name[0]); + LONG result = GetCurrentApplicationUserModelId(&length, value_name); + if (result == ERROR_SUCCESS) { + is_packaged = true; + } + } + } + } + if (!is_packaged && GetModuleFileNameW(nullptr, value_name, sizeof(value_name) / sizeof(value_name[0])) >= sizeof(value_name) / sizeof(value_name[0])) { + // Paths should never be longer than 32767, but just in case. + return false; + } + + LPCWSTR subkey = L"SOFTWARE\\Microsoft\\DirectX\\UserGpuPreferences"; + HKEY hkey = nullptr; + LSTATUS result = RegOpenKeyExW(HKEY_CURRENT_USER, subkey, 0, KEY_READ, &hkey); + if (result != ERROR_SUCCESS) { + return false; + } + + DWORD size = 0; + result = RegGetValueW(hkey, nullptr, value_name, RRF_RT_REG_SZ, nullptr, nullptr, &size); + if (result != ERROR_SUCCESS || size == 0) { + RegCloseKey(hkey); + return false; + } + + Vector<WCHAR> buffer; + buffer.resize(size / sizeof(WCHAR)); + result = RegGetValueW(hkey, nullptr, value_name, RRF_RT_REG_SZ, nullptr, (LPBYTE)buffer.ptrw(), &size); + if (result != ERROR_SUCCESS) { + RegCloseKey(hkey); + return false; + } + + RegCloseKey(hkey); + const String flags = String::utf16((const char16_t *)buffer.ptr(), size / sizeof(WCHAR)); + + for (const String &flag : flags.split(";", false)) { + if (flag == "GpuPreference=1") { + return true; + } + } + return false; +} + OS::DateTime OS_Windows::get_datetime(bool p_utc) const { SYSTEMTIME systemtime; if (p_utc) { @@ -822,7 +901,7 @@ Dictionary OS_Windows::execute_with_pipe(const String &p_path, const List<String Dictionary ret; - String path = p_path.replace("/", "\\"); + String path = p_path.is_absolute_path() ? fix_path(p_path) : p_path; String command = _quote_command_line_argument(path); for (const String &E : p_arguments) { command += " " + _quote_command_line_argument(E); @@ -871,7 +950,19 @@ Dictionary OS_Windows::execute_with_pipe(const String &p_path, const List<String DWORD creation_flags = NORMAL_PRIORITY_CLASS | CREATE_NO_WINDOW; - if (!CreateProcessW(nullptr, (LPWSTR)(command.utf16().ptrw()), nullptr, nullptr, true, creation_flags, nullptr, nullptr, si_w, &pi.pi)) { + Char16String current_dir_name; + size_t str_len = GetCurrentDirectoryW(0, nullptr); + current_dir_name.resize(str_len + 1); + GetCurrentDirectoryW(current_dir_name.size(), (LPWSTR)current_dir_name.ptrw()); + if (current_dir_name.size() >= MAX_PATH) { + Char16String current_short_dir_name; + str_len = GetShortPathNameW((LPCWSTR)current_dir_name.ptr(), nullptr, 0); + current_short_dir_name.resize(str_len); + GetShortPathNameW((LPCWSTR)current_dir_name.ptr(), (LPWSTR)current_short_dir_name.ptrw(), current_short_dir_name.size()); + current_dir_name = current_short_dir_name; + } + + if (!CreateProcessW(nullptr, (LPWSTR)(command.utf16().ptrw()), nullptr, nullptr, true, creation_flags, nullptr, (LPWSTR)current_dir_name.ptr(), si_w, &pi.pi)) { CLEAN_PIPES ERR_FAIL_V_MSG(ret, "Could not create child process: " + command); } @@ -901,7 +992,7 @@ Dictionary OS_Windows::execute_with_pipe(const String &p_path, const List<String } Error OS_Windows::execute(const String &p_path, const List<String> &p_arguments, String *r_pipe, int *r_exitcode, bool read_stderr, Mutex *p_pipe_mutex, bool p_open_console) { - String path = p_path.replace("/", "\\"); + String path = p_path.is_absolute_path() ? fix_path(p_path) : p_path; String command = _quote_command_line_argument(path); for (const String &E : p_arguments) { command += " " + _quote_command_line_argument(E); @@ -939,7 +1030,19 @@ Error OS_Windows::execute(const String &p_path, const List<String> &p_arguments, creation_flags |= CREATE_NO_WINDOW; } - int ret = CreateProcessW(nullptr, (LPWSTR)(command.utf16().ptrw()), nullptr, nullptr, inherit_handles, creation_flags, nullptr, nullptr, si_w, &pi.pi); + Char16String current_dir_name; + size_t str_len = GetCurrentDirectoryW(0, nullptr); + current_dir_name.resize(str_len + 1); + GetCurrentDirectoryW(current_dir_name.size(), (LPWSTR)current_dir_name.ptrw()); + if (current_dir_name.size() >= MAX_PATH) { + Char16String current_short_dir_name; + str_len = GetShortPathNameW((LPCWSTR)current_dir_name.ptr(), nullptr, 0); + current_short_dir_name.resize(str_len); + GetShortPathNameW((LPCWSTR)current_dir_name.ptr(), (LPWSTR)current_short_dir_name.ptrw(), current_short_dir_name.size()); + current_dir_name = current_short_dir_name; + } + + int ret = CreateProcessW(nullptr, (LPWSTR)(command.utf16().ptrw()), nullptr, nullptr, inherit_handles, creation_flags, nullptr, (LPWSTR)current_dir_name.ptr(), si_w, &pi.pi); if (!ret && r_pipe) { CloseHandle(pipe[0]); // Cleanup pipe handles. CloseHandle(pipe[1]); @@ -1003,7 +1106,7 @@ Error OS_Windows::execute(const String &p_path, const List<String> &p_arguments, } Error OS_Windows::create_process(const String &p_path, const List<String> &p_arguments, ProcessID *r_child_id, bool p_open_console) { - String path = p_path.replace("/", "\\"); + String path = p_path.is_absolute_path() ? fix_path(p_path) : p_path; String command = _quote_command_line_argument(path); for (const String &E : p_arguments) { command += " " + _quote_command_line_argument(E); @@ -1022,7 +1125,19 @@ Error OS_Windows::create_process(const String &p_path, const List<String> &p_arg creation_flags |= CREATE_NO_WINDOW; } - int ret = CreateProcessW(nullptr, (LPWSTR)(command.utf16().ptrw()), nullptr, nullptr, false, creation_flags, nullptr, nullptr, si_w, &pi.pi); + Char16String current_dir_name; + size_t str_len = GetCurrentDirectoryW(0, nullptr); + current_dir_name.resize(str_len + 1); + GetCurrentDirectoryW(current_dir_name.size(), (LPWSTR)current_dir_name.ptrw()); + if (current_dir_name.size() >= MAX_PATH) { + Char16String current_short_dir_name; + str_len = GetShortPathNameW((LPCWSTR)current_dir_name.ptr(), nullptr, 0); + current_short_dir_name.resize(str_len); + GetShortPathNameW((LPCWSTR)current_dir_name.ptr(), (LPWSTR)current_short_dir_name.ptrw(), current_short_dir_name.size()); + current_dir_name = current_short_dir_name; + } + + int ret = CreateProcessW(nullptr, (LPWSTR)(command.utf16().ptrw()), nullptr, nullptr, false, creation_flags, nullptr, (LPWSTR)current_dir_name.ptr(), si_w, &pi.pi); ERR_FAIL_COND_V_MSG(ret == 0, ERR_CANT_FORK, "Could not create child process: " + command); ProcessID pid = pi.pi.dwProcessId; @@ -1390,8 +1505,8 @@ Vector<String> OS_Windows::get_system_font_path_for_text(const String &p_font_na continue; } - WCHAR file_path[MAX_PATH]; - hr = loader->GetFilePathFromKey(reference_key, reference_key_size, &file_path[0], MAX_PATH); + WCHAR file_path[32767]; + hr = loader->GetFilePathFromKey(reference_key, reference_key_size, &file_path[0], 32767); if (FAILED(hr)) { continue; } @@ -1469,8 +1584,8 @@ String OS_Windows::get_system_font_path(const String &p_font_name, int p_weight, continue; } - WCHAR file_path[MAX_PATH]; - hr = loader->GetFilePathFromKey(reference_key, reference_key_size, &file_path[0], MAX_PATH); + WCHAR file_path[32767]; + hr = loader->GetFilePathFromKey(reference_key, reference_key_size, &file_path[0], 32767); if (FAILED(hr)) { continue; } @@ -1576,7 +1691,7 @@ Error OS_Windows::shell_show_in_file_manager(String p_path, bool p_open_folder) if (!p_path.is_quoted()) { p_path = p_path.quote(); } - p_path = p_path.replace("/", "\\"); + p_path = fix_path(p_path); INT_PTR ret = OK; if (open_folder) { @@ -1901,6 +2016,19 @@ String OS_Windows::get_system_ca_certificates() { OS_Windows::OS_Windows(HINSTANCE _hInstance) { hInstance = _hInstance; + // Reset CWD to ensure long path is used. + Char16String current_dir_name; + size_t str_len = GetCurrentDirectoryW(0, nullptr); + current_dir_name.resize(str_len + 1); + GetCurrentDirectoryW(current_dir_name.size(), (LPWSTR)current_dir_name.ptrw()); + + Char16String new_current_dir_name; + str_len = GetLongPathNameW((LPCWSTR)current_dir_name.get_data(), nullptr, 0); + new_current_dir_name.resize(str_len + 1); + GetLongPathNameW((LPCWSTR)current_dir_name.get_data(), (LPWSTR)new_current_dir_name.ptrw(), new_current_dir_name.size()); + + SetCurrentDirectoryW((LPCWSTR)new_current_dir_name.get_data()); + #ifndef WINDOWS_SUBSYSTEM_CONSOLE RedirectIOToConsole(); #endif diff --git a/platform/windows/os_windows.h b/platform/windows/os_windows.h index b6a21ed42d..9c7b98d7fd 100644 --- a/platform/windows/os_windows.h +++ b/platform/windows/os_windows.h @@ -172,6 +172,7 @@ public: virtual String get_version() const override; virtual Vector<String> get_video_adapter_driver_info() const override; + virtual bool get_user_prefers_integrated_gpu() const override; virtual void initialize_joypads() override {} diff --git a/platform/windows/windows_utils.cpp b/platform/windows/windows_utils.cpp index 9e0b9eed8a..30743c6900 100644 --- a/platform/windows/windows_utils.cpp +++ b/platform/windows/windows_utils.cpp @@ -155,7 +155,11 @@ Error WindowsUtils::copy_and_rename_pdb(const String &p_dll_path) { } else if (!FileAccess::exists(copy_pdb_path)) { copy_pdb_path = dll_base_dir.path_join(copy_pdb_path.get_file()); } - ERR_FAIL_COND_V_MSG(!FileAccess::exists(copy_pdb_path), FAILED, vformat("File '%s' does not exist.", copy_pdb_path)); + if (!FileAccess::exists(copy_pdb_path)) { + // The PDB file may be distributed separately on purpose, so we don't consider this an error. + WARN_VERBOSE(vformat("PDB file '%s' for library '%s' was not found, skipping copy/rename.", copy_pdb_path, p_dll_path)); + return ERR_SKIP; + } String new_pdb_base_name = p_dll_path.get_file().get_basename() + "_"; diff --git a/scene/2d/audio_stream_player_2d.cpp b/scene/2d/audio_stream_player_2d.cpp index 89a0479de3..7c60e47e64 100644 --- a/scene/2d/audio_stream_player_2d.cpp +++ b/scene/2d/audio_stream_player_2d.cpp @@ -276,10 +276,6 @@ void AudioStreamPlayer2D::_set_playing(bool p_enable) { internal->set_playing(p_enable); } -bool AudioStreamPlayer2D::_is_active() const { - return internal->is_active(); -} - void AudioStreamPlayer2D::_validate_property(PropertyInfo &p_property) const { internal->validate_property(p_property); } @@ -385,8 +381,7 @@ void AudioStreamPlayer2D::_bind_methods() { ClassDB::bind_method(D_METHOD("set_autoplay", "enable"), &AudioStreamPlayer2D::set_autoplay); ClassDB::bind_method(D_METHOD("is_autoplay_enabled"), &AudioStreamPlayer2D::is_autoplay_enabled); - ClassDB::bind_method(D_METHOD("_set_playing", "enable"), &AudioStreamPlayer2D::_set_playing); - ClassDB::bind_method(D_METHOD("_is_active"), &AudioStreamPlayer2D::_is_active); + ClassDB::bind_method(D_METHOD("set_playing", "enable"), &AudioStreamPlayer2D::_set_playing); ClassDB::bind_method(D_METHOD("set_max_distance", "pixels"), &AudioStreamPlayer2D::set_max_distance); ClassDB::bind_method(D_METHOD("get_max_distance"), &AudioStreamPlayer2D::get_max_distance); @@ -415,7 +410,7 @@ void AudioStreamPlayer2D::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "stream", PROPERTY_HINT_RESOURCE_TYPE, "AudioStream"), "set_stream", "get_stream"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "volume_db", PROPERTY_HINT_RANGE, "-80,24,suffix:dB"), "set_volume_db", "get_volume_db"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "pitch_scale", PROPERTY_HINT_RANGE, "0.01,4,0.01,or_greater"), "set_pitch_scale", "get_pitch_scale"); - ADD_PROPERTY(PropertyInfo(Variant::BOOL, "playing", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_EDITOR), "_set_playing", "is_playing"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "playing", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_EDITOR), "set_playing", "is_playing"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "autoplay"), "set_autoplay", "is_autoplay_enabled"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "stream_paused", PROPERTY_HINT_NONE, ""), "set_stream_paused", "get_stream_paused"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "max_distance", PROPERTY_HINT_RANGE, "1,4096,1,or_greater,exp,suffix:px"), "set_max_distance", "get_max_distance"); diff --git a/scene/2d/parallax_2d.cpp b/scene/2d/parallax_2d.cpp index 9dd9d4a376..fdb2d2cdd0 100644 --- a/scene/2d/parallax_2d.cpp +++ b/scene/2d/parallax_2d.cpp @@ -47,9 +47,18 @@ void Parallax2D::_notification(int p_what) { } break; case NOTIFICATION_INTERNAL_PROCESS: { - autoscroll_offset += autoscroll * get_process_delta_time(); - autoscroll_offset = autoscroll_offset.posmodv(repeat_size); + Point2 offset = scroll_offset; + offset += autoscroll * get_process_delta_time(); + if (repeat_size.x) { + offset.x = Math::fposmod(offset.x, repeat_size.x); + } + + if (repeat_size.y) { + offset.y = Math::fposmod(offset.y, repeat_size.y); + } + + scroll_offset = offset; _update_scroll(); } break; @@ -106,14 +115,14 @@ void Parallax2D::_update_scroll() { scroll_ofs *= scroll_scale; if (repeat_size.x) { - real_t mod = Math::fposmod(scroll_ofs.x - scroll_offset.x - autoscroll_offset.x, repeat_size.x * get_scale().x); + real_t mod = Math::fposmod(scroll_ofs.x - scroll_offset.x, repeat_size.x * get_scale().x); scroll_ofs.x = screen_offset.x - mod; } else { scroll_ofs.x = screen_offset.x + scroll_offset.x - scroll_ofs.x; } if (repeat_size.y) { - real_t mod = Math::fposmod(scroll_ofs.y - scroll_offset.y - autoscroll_offset.y, repeat_size.y * get_scale().y); + real_t mod = Math::fposmod(scroll_ofs.y - scroll_offset.y, repeat_size.y * get_scale().y); scroll_ofs.y = screen_offset.y - mod; } else { scroll_ofs.y = screen_offset.y + scroll_offset.y - scroll_ofs.y; @@ -193,7 +202,6 @@ void Parallax2D::set_autoscroll(const Point2 &p_autoscroll) { } autoscroll = p_autoscroll; - autoscroll_offset = Point2(); _update_process(); _update_scroll(); diff --git a/scene/2d/parallax_2d.h b/scene/2d/parallax_2d.h index 5fbc3a20c8..f15e3fa9ff 100644 --- a/scene/2d/parallax_2d.h +++ b/scene/2d/parallax_2d.h @@ -47,7 +47,6 @@ class Parallax2D : public Node2D { Point2 limit_begin = Point2(-DEFAULT_LIMIT, -DEFAULT_LIMIT); Point2 limit_end = Point2(DEFAULT_LIMIT, DEFAULT_LIMIT); Point2 autoscroll; - Point2 autoscroll_offset; bool follow_viewport = true; bool ignore_camera_scroll = false; diff --git a/scene/2d/remote_transform_2d.cpp b/scene/2d/remote_transform_2d.cpp index 5ea5098475..920f5720fa 100644 --- a/scene/2d/remote_transform_2d.cpp +++ b/scene/2d/remote_transform_2d.cpp @@ -114,6 +114,16 @@ void RemoteTransform2D::_notification(int p_what) { _update_cache(); } break; + case NOTIFICATION_RESET_PHYSICS_INTERPOLATION: { + if (cache.is_valid()) { + _update_remote(); + Node2D *n = Object::cast_to<Node2D>(ObjectDB::get_instance(cache)); + if (n) { + n->reset_physics_interpolation(); + } + } + } break; + case NOTIFICATION_LOCAL_TRANSFORM_CHANGED: case NOTIFICATION_TRANSFORM_CHANGED: { if (!is_inside_tree()) { diff --git a/scene/2d/tile_map_layer.cpp b/scene/2d/tile_map_layer.cpp index 7b125a6895..ba0958e74b 100644 --- a/scene/2d/tile_map_layer.cpp +++ b/scene/2d/tile_map_layer.cpp @@ -411,7 +411,7 @@ void TileMapLayer::_rendering_update(bool p_force_cleanup) { } // ----------- Occluders processing ----------- - if (forced_cleanup) { + if (forced_cleanup || !occlusion_enabled) { // Clean everything. for (KeyValue<Vector2i, CellData> &kv : tile_map_layer_data) { _rendering_occluders_clear_cell(kv.value); @@ -433,7 +433,7 @@ void TileMapLayer::_rendering_update(bool p_force_cleanup) { // ----------- // Mark the rendering state as up to date. - _rendering_was_cleaned_up = forced_cleanup; + _rendering_was_cleaned_up = forced_cleanup || !occlusion_enabled; } void TileMapLayer::_rendering_notification(int p_what) { @@ -957,7 +957,7 @@ void TileMapLayer::_navigation_update(bool p_force_cleanup) { // Create a dedicated map for each layer. RID new_layer_map = ns->map_create(); // Set the default NavigationPolygon cell_size on the new map as a mismatch causes an error. - ns->map_set_cell_size(new_layer_map, 1.0); + ns->map_set_cell_size(new_layer_map, NavigationDefaults2D::navmesh_cell_size); ns->map_set_active(new_layer_map, true); navigation_map_override = new_layer_map; } @@ -1828,6 +1828,9 @@ void TileMapLayer::_bind_methods() { ClassDB::bind_method(D_METHOD("set_collision_visibility_mode", "visibility_mode"), &TileMapLayer::set_collision_visibility_mode); ClassDB::bind_method(D_METHOD("get_collision_visibility_mode"), &TileMapLayer::get_collision_visibility_mode); + ClassDB::bind_method(D_METHOD("set_occlusion_enabled", "enabled"), &TileMapLayer::set_occlusion_enabled); + ClassDB::bind_method(D_METHOD("is_occlusion_enabled"), &TileMapLayer::is_occlusion_enabled); + ClassDB::bind_method(D_METHOD("set_navigation_enabled", "enabled"), &TileMapLayer::set_navigation_enabled); ClassDB::bind_method(D_METHOD("is_navigation_enabled"), &TileMapLayer::is_navigation_enabled); ClassDB::bind_method(D_METHOD("set_navigation_map", "map"), &TileMapLayer::set_navigation_map); @@ -1843,6 +1846,7 @@ void TileMapLayer::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::BOOL, "enabled"), "set_enabled", "is_enabled"); ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "tile_set", PROPERTY_HINT_RESOURCE_TYPE, "TileSet"), "set_tile_set", "get_tile_set"); ADD_GROUP("Rendering", ""); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "occlusion_enabled"), "set_occlusion_enabled", "is_occlusion_enabled"); ADD_PROPERTY(PropertyInfo(Variant::INT, "y_sort_origin"), "set_y_sort_origin", "get_y_sort_origin"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "x_draw_order_reversed"), "set_x_draw_order_reversed", "is_x_draw_order_reversed"); ADD_PROPERTY(PropertyInfo(Variant::INT, "rendering_quadrant_size"), "set_rendering_quadrant_size", "get_rendering_quadrant_size"); @@ -2960,6 +2964,20 @@ TileMapLayer::DebugVisibilityMode TileMapLayer::get_collision_visibility_mode() return collision_visibility_mode; } +void TileMapLayer::set_occlusion_enabled(bool p_enabled) { + if (occlusion_enabled == p_enabled) { + return; + } + occlusion_enabled = p_enabled; + dirty.flags[DIRTY_FLAGS_LAYER_OCCLUSION_ENABLED] = true; + _queue_internal_update(); + emit_signal(CoreStringName(changed)); +} + +bool TileMapLayer::is_occlusion_enabled() const { + return occlusion_enabled; +} + void TileMapLayer::set_navigation_enabled(bool p_enabled) { if (navigation_enabled == p_enabled) { return; diff --git a/scene/2d/tile_map_layer.h b/scene/2d/tile_map_layer.h index 1a6d182094..2a986667bd 100644 --- a/scene/2d/tile_map_layer.h +++ b/scene/2d/tile_map_layer.h @@ -254,6 +254,7 @@ public: DIRTY_FLAGS_LAYER_COLLISION_ENABLED, DIRTY_FLAGS_LAYER_USE_KINEMATIC_BODIES, DIRTY_FLAGS_LAYER_COLLISION_VISIBILITY_MODE, + DIRTY_FLAGS_LAYER_OCCLUSION_ENABLED, DIRTY_FLAGS_LAYER_NAVIGATION_ENABLED, DIRTY_FLAGS_LAYER_NAVIGATION_MAP, DIRTY_FLAGS_LAYER_NAVIGATION_VISIBILITY_MODE, @@ -288,6 +289,8 @@ private: bool use_kinematic_bodies = false; DebugVisibilityMode collision_visibility_mode = DEBUG_VISIBILITY_MODE_DEFAULT; + bool occlusion_enabled = true; + bool navigation_enabled = true; RID navigation_map_override; DebugVisibilityMode navigation_visibility_mode = DEBUG_VISIBILITY_MODE_DEFAULT; @@ -497,6 +500,9 @@ public: void set_collision_visibility_mode(DebugVisibilityMode p_show_collision); DebugVisibilityMode get_collision_visibility_mode() const; + void set_occlusion_enabled(bool p_enabled); + bool is_occlusion_enabled() const; + void set_navigation_enabled(bool p_enabled); bool is_navigation_enabled() const; void set_navigation_map(RID p_map); diff --git a/scene/3d/audio_stream_player_3d.cpp b/scene/3d/audio_stream_player_3d.cpp index 4d3f494ccf..591528b915 100644 --- a/scene/3d/audio_stream_player_3d.cpp +++ b/scene/3d/audio_stream_player_3d.cpp @@ -596,10 +596,6 @@ void AudioStreamPlayer3D::_set_playing(bool p_enable) { internal->set_playing(p_enable); } -bool AudioStreamPlayer3D::_is_active() const { - return internal->is_active(); -} - void AudioStreamPlayer3D::_validate_property(PropertyInfo &p_property) const { internal->validate_property(p_property); } @@ -779,8 +775,7 @@ void AudioStreamPlayer3D::_bind_methods() { ClassDB::bind_method(D_METHOD("set_autoplay", "enable"), &AudioStreamPlayer3D::set_autoplay); ClassDB::bind_method(D_METHOD("is_autoplay_enabled"), &AudioStreamPlayer3D::is_autoplay_enabled); - ClassDB::bind_method(D_METHOD("_set_playing", "enable"), &AudioStreamPlayer3D::_set_playing); - ClassDB::bind_method(D_METHOD("_is_active"), &AudioStreamPlayer3D::_is_active); + ClassDB::bind_method(D_METHOD("set_playing", "enable"), &AudioStreamPlayer3D::_set_playing); ClassDB::bind_method(D_METHOD("set_max_distance", "meters"), &AudioStreamPlayer3D::set_max_distance); ClassDB::bind_method(D_METHOD("get_max_distance"), &AudioStreamPlayer3D::get_max_distance); @@ -830,7 +825,7 @@ void AudioStreamPlayer3D::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "unit_size", PROPERTY_HINT_RANGE, "0.1,100,0.01,or_greater"), "set_unit_size", "get_unit_size"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "max_db", PROPERTY_HINT_RANGE, "-24,6,suffix:dB"), "set_max_db", "get_max_db"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "pitch_scale", PROPERTY_HINT_RANGE, "0.01,4,0.01,or_greater"), "set_pitch_scale", "get_pitch_scale"); - ADD_PROPERTY(PropertyInfo(Variant::BOOL, "playing", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_EDITOR), "_set_playing", "is_playing"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "playing", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_EDITOR), "set_playing", "is_playing"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "autoplay"), "set_autoplay", "is_autoplay_enabled"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "stream_paused", PROPERTY_HINT_NONE, ""), "set_stream_paused", "get_stream_paused"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "max_distance", PROPERTY_HINT_RANGE, "0,4096,0.01,or_greater,suffix:m"), "set_max_distance", "get_max_distance"); diff --git a/scene/3d/cpu_particles_3d.cpp b/scene/3d/cpu_particles_3d.cpp index 03fe5e1fad..acbc443a93 100644 --- a/scene/3d/cpu_particles_3d.cpp +++ b/scene/3d/cpu_particles_3d.cpp @@ -448,6 +448,10 @@ void CPUParticles3D::set_emission_ring_inner_radius(real_t p_radius) { emission_ring_inner_radius = p_radius; } +void CPUParticles3D::set_emission_ring_cone_angle(real_t p_angle) { + emission_ring_cone_angle = p_angle; +} + void CPUParticles3D::set_scale_curve_x(Ref<Curve> p_scale_curve) { scale_curve_x = p_scale_curve; } @@ -501,6 +505,10 @@ real_t CPUParticles3D::get_emission_ring_inner_radius() const { return emission_ring_inner_radius; } +real_t CPUParticles3D::get_emission_ring_cone_angle() const { + return emission_ring_cone_angle; +} + CPUParticles3D::EmissionShape CPUParticles3D::get_emission_shape() const { return emission_shape; } @@ -878,8 +886,14 @@ void CPUParticles3D::_particles_process(double p_delta) { } } break; case EMISSION_SHAPE_RING: { + real_t radius_clamped = MAX(0.001, emission_ring_radius); + real_t top_radius = MAX(radius_clamped - Math::tan(Math::deg_to_rad(90.0 - emission_ring_cone_angle)) * emission_ring_height, 0.0); + real_t y_pos = Math::randf(); + real_t skew = MAX(MIN(radius_clamped, top_radius) / MAX(radius_clamped, top_radius), 0.5); + y_pos = radius_clamped < top_radius ? Math::pow(y_pos, skew) : 1.0 - Math::pow(y_pos, skew); real_t ring_random_angle = Math::randf() * Math_TAU; - real_t ring_random_radius = Math::sqrt(Math::randf() * (emission_ring_radius * emission_ring_radius - emission_ring_inner_radius * emission_ring_inner_radius) + emission_ring_inner_radius * emission_ring_inner_radius); + real_t ring_random_radius = Math::sqrt(Math::randf() * (radius_clamped * radius_clamped - emission_ring_inner_radius * emission_ring_inner_radius) + emission_ring_inner_radius * emission_ring_inner_radius); + ring_random_radius = Math::lerp(ring_random_radius, ring_random_radius * (top_radius / radius_clamped), y_pos); Vector3 axis = emission_ring_axis == Vector3(0.0, 0.0, 0.0) ? Vector3(0.0, 0.0, 1.0) : emission_ring_axis.normalized(); Vector3 ortho_axis; if (axis.abs() == Vector3(1.0, 0.0, 0.0)) { @@ -890,7 +904,7 @@ void CPUParticles3D::_particles_process(double p_delta) { ortho_axis = ortho_axis.normalized(); ortho_axis.rotate(axis, ring_random_angle); ortho_axis = ortho_axis.normalized(); - p.transform.origin = ortho_axis * ring_random_radius + (Math::randf() * emission_ring_height - emission_ring_height / 2.0) * axis; + p.transform.origin = ortho_axis * ring_random_radius + (y_pos * emission_ring_height - emission_ring_height / 2.0) * axis; } break; case EMISSION_SHAPE_MAX: { // Max value for validity check. break; @@ -1550,6 +1564,9 @@ void CPUParticles3D::_bind_methods() { ClassDB::bind_method(D_METHOD("set_emission_ring_inner_radius", "inner_radius"), &CPUParticles3D::set_emission_ring_inner_radius); ClassDB::bind_method(D_METHOD("get_emission_ring_inner_radius"), &CPUParticles3D::get_emission_ring_inner_radius); + ClassDB::bind_method(D_METHOD("set_emission_ring_cone_angle", "cone_angle"), &CPUParticles3D::set_emission_ring_cone_angle); + ClassDB::bind_method(D_METHOD("get_emission_ring_cone_angle"), &CPUParticles3D::get_emission_ring_cone_angle); + ClassDB::bind_method(D_METHOD("get_gravity"), &CPUParticles3D::get_gravity); ClassDB::bind_method(D_METHOD("set_gravity", "accel_vec"), &CPUParticles3D::set_gravity); @@ -1577,9 +1594,10 @@ void CPUParticles3D::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::PACKED_VECTOR3_ARRAY, "emission_normals"), "set_emission_normals", "get_emission_normals"); ADD_PROPERTY(PropertyInfo(Variant::PACKED_COLOR_ARRAY, "emission_colors"), "set_emission_colors", "get_emission_colors"); ADD_PROPERTY(PropertyInfo(Variant::VECTOR3, "emission_ring_axis"), "set_emission_ring_axis", "get_emission_ring_axis"); - ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "emission_ring_height"), "set_emission_ring_height", "get_emission_ring_height"); - ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "emission_ring_radius"), "set_emission_ring_radius", "get_emission_ring_radius"); - ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "emission_ring_inner_radius"), "set_emission_ring_inner_radius", "get_emission_ring_inner_radius"); + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "emission_ring_height", PROPERTY_HINT_RANGE, "0,1000,0.01,or_greater"), "set_emission_ring_height", "get_emission_ring_height"); + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "emission_ring_radius", PROPERTY_HINT_RANGE, "0,1000,0.01,or_greater"), "set_emission_ring_radius", "get_emission_ring_radius"); + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "emission_ring_inner_radius", PROPERTY_HINT_RANGE, "0,1000,0.01,or_greater"), "set_emission_ring_inner_radius", "get_emission_ring_inner_radius"); + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "emission_ring_cone_angle", PROPERTY_HINT_RANGE, "0,90,0.01,degrees"), "set_emission_ring_cone_angle", "get_emission_ring_cone_angle"); ADD_GROUP("Particle Flags", "particle_flag_"); ADD_PROPERTYI(PropertyInfo(Variant::BOOL, "particle_flag_align_y"), "set_particle_flag", "get_particle_flag", PARTICLE_FLAG_ALIGN_Y_TO_VELOCITY); ADD_PROPERTYI(PropertyInfo(Variant::BOOL, "particle_flag_rotate_y"), "set_particle_flag", "get_particle_flag", PARTICLE_FLAG_ROTATE_Y); @@ -1716,6 +1734,7 @@ CPUParticles3D::CPUParticles3D() { set_emission_ring_height(1); set_emission_ring_radius(1); set_emission_ring_inner_radius(0); + set_emission_ring_cone_angle(90); set_gravity(Vector3(0, -9.8, 0)); diff --git a/scene/3d/cpu_particles_3d.h b/scene/3d/cpu_particles_3d.h index 82ea4bbef3..978bb64e71 100644 --- a/scene/3d/cpu_particles_3d.h +++ b/scene/3d/cpu_particles_3d.h @@ -178,6 +178,7 @@ private: real_t emission_ring_height = 0.0; real_t emission_ring_radius = 0.0; real_t emission_ring_inner_radius = 0.0; + real_t emission_ring_cone_angle = 0.0; Ref<Curve> scale_curve_x; Ref<Curve> scale_curve_y; @@ -282,6 +283,7 @@ public: void set_emission_ring_height(real_t p_height); void set_emission_ring_radius(real_t p_radius); void set_emission_ring_inner_radius(real_t p_radius); + void set_emission_ring_cone_angle(real_t p_angle); void set_scale_curve_x(Ref<Curve> p_scale_curve); void set_scale_curve_y(Ref<Curve> p_scale_curve); void set_scale_curve_z(Ref<Curve> p_scale_curve); @@ -297,6 +299,7 @@ public: real_t get_emission_ring_height() const; real_t get_emission_ring_radius() const; real_t get_emission_ring_inner_radius() const; + real_t get_emission_ring_cone_angle() const; Ref<Curve> get_scale_curve_x() const; Ref<Curve> get_scale_curve_y() const; Ref<Curve> get_scale_curve_z() const; diff --git a/scene/3d/physics/vehicle_body_3d.cpp b/scene/3d/physics/vehicle_body_3d.cpp index c23032d3b9..b4c321cf5f 100644 --- a/scene/3d/physics/vehicle_body_3d.cpp +++ b/scene/3d/physics/vehicle_body_3d.cpp @@ -219,6 +219,14 @@ bool VehicleWheel3D::is_in_contact() const { return m_raycastInfo.m_isInContact; } +Vector3 VehicleWheel3D::get_contact_point() const { + return m_raycastInfo.m_contactPointWS; +} + +Vector3 VehicleWheel3D::get_contact_normal() const { + return m_raycastInfo.m_contactNormalWS; +} + Node3D *VehicleWheel3D::get_contact_body() const { return m_raycastInfo.m_groundObject; } @@ -256,6 +264,8 @@ void VehicleWheel3D::_bind_methods() { ClassDB::bind_method(D_METHOD("is_in_contact"), &VehicleWheel3D::is_in_contact); ClassDB::bind_method(D_METHOD("get_contact_body"), &VehicleWheel3D::get_contact_body); + ClassDB::bind_method(D_METHOD("get_contact_point"), &VehicleWheel3D::get_contact_point); + ClassDB::bind_method(D_METHOD("get_contact_normal"), &VehicleWheel3D::get_contact_normal); ClassDB::bind_method(D_METHOD("set_roll_influence", "roll_influence"), &VehicleWheel3D::set_roll_influence); ClassDB::bind_method(D_METHOD("get_roll_influence"), &VehicleWheel3D::get_roll_influence); diff --git a/scene/3d/physics/vehicle_body_3d.h b/scene/3d/physics/vehicle_body_3d.h index def9984440..24f120ed26 100644 --- a/scene/3d/physics/vehicle_body_3d.h +++ b/scene/3d/physics/vehicle_body_3d.h @@ -130,6 +130,10 @@ public: bool is_in_contact() const; + Vector3 get_contact_point() const; + + Vector3 get_contact_normal() const; + Node3D *get_contact_body() const; void set_roll_influence(real_t p_value); diff --git a/scene/3d/remote_transform_3d.cpp b/scene/3d/remote_transform_3d.cpp index 8d6e717132..e580882c46 100644 --- a/scene/3d/remote_transform_3d.cpp +++ b/scene/3d/remote_transform_3d.cpp @@ -113,6 +113,16 @@ void RemoteTransform3D::_notification(int p_what) { _update_cache(); } break; + case NOTIFICATION_RESET_PHYSICS_INTERPOLATION: { + if (cache.is_valid()) { + _update_remote(); + Node3D *n = Object::cast_to<Node3D>(ObjectDB::get_instance(cache)); + if (n) { + n->reset_physics_interpolation(); + } + } + } break; + case NOTIFICATION_LOCAL_TRANSFORM_CHANGED: case NOTIFICATION_TRANSFORM_CHANGED: { if (!is_inside_tree()) { diff --git a/scene/3d/voxelizer.h b/scene/3d/voxelizer.h index 6ea1cfdbb0..08d018eee9 100644 --- a/scene/3d/voxelizer.h +++ b/scene/3d/voxelizer.h @@ -35,9 +35,8 @@ class Voxelizer { private: - enum { + enum : uint32_t { CHILD_EMPTY = 0xFFFFFFFF - }; struct Cell { diff --git a/scene/3d/xr_hand_modifier_3d.cpp b/scene/3d/xr_hand_modifier_3d.cpp index baaa9eee48..aa63fb623f 100644 --- a/scene/3d/xr_hand_modifier_3d.cpp +++ b/scene/3d/xr_hand_modifier_3d.cpp @@ -207,6 +207,11 @@ void XRHandModifier3D::_process_modification() { // Apply previous relative transforms if they are stored. for (int joint = 0; joint < XRHandTracker::HAND_JOINT_MAX; joint++) { + const int bone = joints[joint].bone; + if (bone == -1) { + continue; + } + if (bone_update == BONE_UPDATE_FULL) { skeleton->set_bone_pose_position(joints[joint].bone, previous_relative_transforms[joint].origin); } diff --git a/scene/animation/animation_mixer.cpp b/scene/animation/animation_mixer.cpp index 20dd12f8c3..0ab4dbe470 100644 --- a/scene/animation/animation_mixer.cpp +++ b/scene/animation/animation_mixer.cpp @@ -947,14 +947,6 @@ void AnimationMixer::_process_animation(double p_delta, bool p_update_only) { clear_animation_instances(); } -Variant AnimationMixer::post_process_key_value(const Ref<Animation> &p_anim, int p_track, Variant p_value, ObjectID p_object_id, int p_object_sub_idx) { - Variant res; - if (GDVIRTUAL_CALL(_post_process_key_value, p_anim, p_track, p_value, p_object_id, p_object_sub_idx, res)) { - return res; - } - return _post_process_key_value(p_anim, p_track, p_value, p_object_id, p_object_sub_idx); -} - Variant AnimationMixer::_post_process_key_value(const Ref<Animation> &p_anim, int p_track, Variant p_value, ObjectID p_object_id, int p_object_sub_idx) { #ifndef _3D_DISABLED switch (p_anim->track_get_type(p_track)) { @@ -974,6 +966,17 @@ Variant AnimationMixer::_post_process_key_value(const Ref<Animation> &p_anim, in return p_value; } +Variant AnimationMixer::post_process_key_value(const Ref<Animation> &p_anim, int p_track, Variant p_value, ObjectID p_object_id, int p_object_sub_idx) { + if (is_GDVIRTUAL_CALL_post_process_key_value) { + Variant res; + if (GDVIRTUAL_CALL(_post_process_key_value, p_anim, p_track, p_value, p_object_id, p_object_sub_idx, res)) { + return res; + } + is_GDVIRTUAL_CALL_post_process_key_value = false; + } + return _post_process_key_value(p_anim, p_track, p_value, p_object_id, p_object_sub_idx); +} + void AnimationMixer::_blend_init() { // Check all tracks, see if they need modification. root_motion_position = Vector3(0, 0, 0); @@ -1080,22 +1083,26 @@ void AnimationMixer::_blend_calc_total_weight() { for (const AnimationInstance &ai : animation_instances) { Ref<Animation> a = ai.animation_data.animation; real_t weight = ai.playback_info.weight; - Vector<real_t> track_weights = ai.playback_info.track_weights; - Vector<Animation::TypeHash> processed_hashes; - for (int i = 0; i < a->get_track_count(); i++) { - if (!a->track_is_enabled(i)) { + const real_t *track_weights_ptr = ai.playback_info.track_weights.ptr(); + int track_weights_count = ai.playback_info.track_weights.size(); + static LocalVector<Animation::TypeHash> processed_hashes; + processed_hashes.clear(); + const Vector<Animation::Track *> tracks = a->get_tracks(); + for (const Animation::Track *animation_track : tracks) { + if (!animation_track->enabled) { continue; } - Animation::TypeHash thash = a->track_get_type_hash(i); - if (!track_cache.has(thash) || processed_hashes.has(thash)) { + Animation::TypeHash thash = animation_track->thash; + TrackCache **track_ptr = track_cache.getptr(thash); + if (track_ptr == nullptr || processed_hashes.has(thash)) { // No path, but avoid error spamming. // Or, there is the case different track type with same path; These can be distinguished by hash. So don't add the weight doubly. continue; } - TrackCache *track = track_cache[thash]; + TrackCache *track = *track_ptr; int blend_idx = track_map[track->path]; ERR_CONTINUE(blend_idx < 0 || blend_idx >= track_count); - real_t blend = blend_idx < track_weights.size() ? track_weights[blend_idx] * weight : weight; + real_t blend = blend_idx < track_weights_count ? track_weights_ptr[blend_idx] * weight : weight; track->total_weight += blend; processed_hashes.push_back(thash); } @@ -1115,26 +1122,33 @@ void AnimationMixer::_blend_process(double p_delta, bool p_update_only) { Animation::LoopedFlag looped_flag = ai.playback_info.looped_flag; bool is_external_seeking = ai.playback_info.is_external_seeking; real_t weight = ai.playback_info.weight; - Vector<real_t> track_weights = ai.playback_info.track_weights; + const real_t *track_weights_ptr = ai.playback_info.track_weights.ptr(); + int track_weights_count = ai.playback_info.track_weights.size(); bool backward = signbit(delta); // This flag is used by the root motion calculates or detecting the end of audio stream. bool seeked_backward = signbit(p_delta); #ifndef _3D_DISABLED bool calc_root = !seeked || is_external_seeking; #endif // _3D_DISABLED - - for (int i = 0; i < a->get_track_count(); i++) { - if (!a->track_is_enabled(i)) { + const Vector<Animation::Track *> tracks = a->get_tracks(); + Animation::Track *const *tracks_ptr = tracks.ptr(); + real_t a_length = a->get_length(); + int count = tracks.size(); + for (int i = 0; i < count; i++) { + const Animation::Track *animation_track = tracks_ptr[i]; + if (!animation_track->enabled) { continue; } - Animation::TypeHash thash = a->track_get_type_hash(i); - if (!track_cache.has(thash)) { + Animation::TypeHash thash = animation_track->thash; + TrackCache **track_ptr = track_cache.getptr(thash); + if (track_ptr == nullptr) { continue; // No path, but avoid error spamming. } - TrackCache *track = track_cache[thash]; - ERR_CONTINUE(!track_map.has(track->path)); - int blend_idx = track_map[track->path]; + TrackCache *track = *track_ptr; + int *blend_idx_ptr = track_map.getptr(track->path); + ERR_CONTINUE(blend_idx_ptr == nullptr); + int blend_idx = *blend_idx_ptr; ERR_CONTINUE(blend_idx < 0 || blend_idx >= track_count); - real_t blend = blend_idx < track_weights.size() ? track_weights[blend_idx] * weight : weight; + real_t blend = blend_idx < track_weights_count ? track_weights_ptr[blend_idx] * weight : weight; if (!deterministic) { // If non-deterministic, do normalization. // It would be better to make this if statement outside the for loop, but come here since too much code... @@ -1143,8 +1157,8 @@ void AnimationMixer::_blend_process(double p_delta, bool p_update_only) { } blend = blend / track->total_weight; } - Animation::TrackType ttype = a->track_get_type(i); - track->root_motion = root_motion_track == a->track_get_path(i); + Animation::TrackType ttype = animation_track->type; + track->root_motion = root_motion_track == animation_track->path; switch (ttype) { case Animation::TYPE_POSITION_3D: { #ifndef _3D_DISABLED @@ -1161,26 +1175,26 @@ void AnimationMixer::_blend_process(double p_delta, bool p_update_only) { prev_time = 0; } break; case Animation::LOOP_LINEAR: { - prev_time = Math::fposmod(prev_time, (double)a->get_length()); + prev_time = Math::fposmod(prev_time, (double)a_length); } break; case Animation::LOOP_PINGPONG: { - prev_time = Math::pingpong(prev_time, (double)a->get_length()); + prev_time = Math::pingpong(prev_time, (double)a_length); } break; default: break; } } } else { - if (Animation::is_greater_approx(prev_time, (double)a->get_length())) { + if (Animation::is_greater_approx(prev_time, (double)a_length)) { switch (a->get_loop_mode()) { case Animation::LOOP_NONE: { - prev_time = (double)a->get_length(); + prev_time = (double)a_length; } break; case Animation::LOOP_LINEAR: { - prev_time = Math::fposmod(prev_time, (double)a->get_length()); + prev_time = Math::fposmod(prev_time, (double)a_length); } break; case Animation::LOOP_PINGPONG: { - prev_time = Math::pingpong(prev_time, (double)a->get_length()); + prev_time = Math::pingpong(prev_time, (double)a_length); } break; default: break; @@ -1195,7 +1209,7 @@ void AnimationMixer::_blend_process(double p_delta, bool p_update_only) { continue; } loc[0] = post_process_key_value(a, i, loc[0], t->object_id, t->bone_idx); - a->try_position_track_interpolate(i, (double)a->get_length(), &loc[1]); + a->try_position_track_interpolate(i, (double)a_length, &loc[1]); loc[1] = post_process_key_value(a, i, loc[1], t->object_id, t->bone_idx); root_motion_cache.loc += (loc[1] - loc[0]) * blend; prev_time = 0; @@ -1210,7 +1224,7 @@ void AnimationMixer::_blend_process(double p_delta, bool p_update_only) { a->try_position_track_interpolate(i, 0, &loc[1]); loc[1] = post_process_key_value(a, i, loc[1], t->object_id, t->bone_idx); root_motion_cache.loc += (loc[1] - loc[0]) * blend; - prev_time = (double)a->get_length(); + prev_time = (double)a_length; } } Error err = a->try_position_track_interpolate(i, prev_time, &loc[0]); @@ -1221,7 +1235,7 @@ void AnimationMixer::_blend_process(double p_delta, bool p_update_only) { a->try_position_track_interpolate(i, time, &loc[1]); loc[1] = post_process_key_value(a, i, loc[1], t->object_id, t->bone_idx); root_motion_cache.loc += (loc[1] - loc[0]) * blend; - prev_time = !backward ? 0 : (double)a->get_length(); + prev_time = !backward ? 0 : (double)a_length; } { Vector3 loc; @@ -1249,26 +1263,26 @@ void AnimationMixer::_blend_process(double p_delta, bool p_update_only) { prev_time = 0; } break; case Animation::LOOP_LINEAR: { - prev_time = Math::fposmod(prev_time, (double)a->get_length()); + prev_time = Math::fposmod(prev_time, (double)a_length); } break; case Animation::LOOP_PINGPONG: { - prev_time = Math::pingpong(prev_time, (double)a->get_length()); + prev_time = Math::pingpong(prev_time, (double)a_length); } break; default: break; } } } else { - if (Animation::is_greater_approx(prev_time, (double)a->get_length())) { + if (Animation::is_greater_approx(prev_time, (double)a_length)) { switch (a->get_loop_mode()) { case Animation::LOOP_NONE: { - prev_time = (double)a->get_length(); + prev_time = (double)a_length; } break; case Animation::LOOP_LINEAR: { - prev_time = Math::fposmod(prev_time, (double)a->get_length()); + prev_time = Math::fposmod(prev_time, (double)a_length); } break; case Animation::LOOP_PINGPONG: { - prev_time = Math::pingpong(prev_time, (double)a->get_length()); + prev_time = Math::pingpong(prev_time, (double)a_length); } break; default: break; @@ -1283,7 +1297,7 @@ void AnimationMixer::_blend_process(double p_delta, bool p_update_only) { continue; } rot[0] = post_process_key_value(a, i, rot[0], t->object_id, t->bone_idx); - a->try_rotation_track_interpolate(i, (double)a->get_length(), &rot[1]); + a->try_rotation_track_interpolate(i, (double)a_length, &rot[1]); rot[1] = post_process_key_value(a, i, rot[1], t->object_id, t->bone_idx); root_motion_cache.rot = (root_motion_cache.rot * Quaternion().slerp(rot[0].inverse() * rot[1], blend)).normalized(); prev_time = 0; @@ -1297,7 +1311,7 @@ void AnimationMixer::_blend_process(double p_delta, bool p_update_only) { rot[0] = post_process_key_value(a, i, rot[0], t->object_id, t->bone_idx); a->try_rotation_track_interpolate(i, 0, &rot[1]); root_motion_cache.rot = (root_motion_cache.rot * Quaternion().slerp(rot[0].inverse() * rot[1], blend)).normalized(); - prev_time = (double)a->get_length(); + prev_time = (double)a_length; } } Error err = a->try_rotation_track_interpolate(i, prev_time, &rot[0]); @@ -1308,7 +1322,7 @@ void AnimationMixer::_blend_process(double p_delta, bool p_update_only) { a->try_rotation_track_interpolate(i, time, &rot[1]); rot[1] = post_process_key_value(a, i, rot[1], t->object_id, t->bone_idx); root_motion_cache.rot = (root_motion_cache.rot * Quaternion().slerp(rot[0].inverse() * rot[1], blend)).normalized(); - prev_time = !backward ? 0 : (double)a->get_length(); + prev_time = !backward ? 0 : (double)a_length; } { Quaternion rot; @@ -1336,26 +1350,26 @@ void AnimationMixer::_blend_process(double p_delta, bool p_update_only) { prev_time = 0; } break; case Animation::LOOP_LINEAR: { - prev_time = Math::fposmod(prev_time, (double)a->get_length()); + prev_time = Math::fposmod(prev_time, (double)a_length); } break; case Animation::LOOP_PINGPONG: { - prev_time = Math::pingpong(prev_time, (double)a->get_length()); + prev_time = Math::pingpong(prev_time, (double)a_length); } break; default: break; } } } else { - if (Animation::is_greater_approx(prev_time, (double)a->get_length())) { + if (Animation::is_greater_approx(prev_time, (double)a_length)) { switch (a->get_loop_mode()) { case Animation::LOOP_NONE: { - prev_time = (double)a->get_length(); + prev_time = (double)a_length; } break; case Animation::LOOP_LINEAR: { - prev_time = Math::fposmod(prev_time, (double)a->get_length()); + prev_time = Math::fposmod(prev_time, (double)a_length); } break; case Animation::LOOP_PINGPONG: { - prev_time = Math::pingpong(prev_time, (double)a->get_length()); + prev_time = Math::pingpong(prev_time, (double)a_length); } break; default: break; @@ -1370,7 +1384,7 @@ void AnimationMixer::_blend_process(double p_delta, bool p_update_only) { continue; } scale[0] = post_process_key_value(a, i, scale[0], t->object_id, t->bone_idx); - a->try_scale_track_interpolate(i, (double)a->get_length(), &scale[1]); + a->try_scale_track_interpolate(i, (double)a_length, &scale[1]); root_motion_cache.scale += (scale[1] - scale[0]) * blend; scale[1] = post_process_key_value(a, i, scale[1], t->object_id, t->bone_idx); prev_time = 0; @@ -1385,7 +1399,7 @@ void AnimationMixer::_blend_process(double p_delta, bool p_update_only) { a->try_scale_track_interpolate(i, 0, &scale[1]); scale[1] = post_process_key_value(a, i, scale[1], t->object_id, t->bone_idx); root_motion_cache.scale += (scale[1] - scale[0]) * blend; - prev_time = (double)a->get_length(); + prev_time = (double)a_length; } } Error err = a->try_scale_track_interpolate(i, prev_time, &scale[0]); @@ -1396,7 +1410,7 @@ void AnimationMixer::_blend_process(double p_delta, bool p_update_only) { a->try_scale_track_interpolate(i, time, &scale[1]); scale[1] = post_process_key_value(a, i, scale[1], t->object_id, t->bone_idx); root_motion_cache.scale += (scale[1] - scale[0]) * blend; - prev_time = !backward ? 0 : (double)a->get_length(); + prev_time = !backward ? 0 : (double)a_length; } { Vector3 scale; @@ -1560,7 +1574,7 @@ void AnimationMixer::_blend_process(double p_delta, bool p_update_only) { } PlayingAudioTrackInfo &track_info = t->playing_streams[oid]; - track_info.length = a->get_length(); + track_info.length = a_length; track_info.time = time; track_info.volume += blend; track_info.loop = a->get_loop_mode() != Animation::LOOP_NONE; @@ -1679,7 +1693,7 @@ void AnimationMixer::_blend_process(double p_delta, bool p_update_only) { at_anim_pos = Math::fposmod(time - pos, (double)anim->get_length()); // Seek to loop. } break; case Animation::LOOP_PINGPONG: { - at_anim_pos = Math::pingpong(time - pos, (double)a->get_length()); + at_anim_pos = Math::pingpong(time - pos, (double)a_length); } break; default: break; @@ -1717,6 +1731,7 @@ void AnimationMixer::_blend_process(double p_delta, bool p_update_only) { } } } + is_GDVIRTUAL_CALL_post_process_key_value = true; } void AnimationMixer::_blend_apply() { diff --git a/scene/animation/animation_mixer.h b/scene/animation/animation_mixer.h index c029c68ae1..5482197fbd 100644 --- a/scene/animation/animation_mixer.h +++ b/scene/animation/animation_mixer.h @@ -48,6 +48,7 @@ class AnimationMixer : public Node { #endif // TOOLS_ENABLED bool reset_on_save = true; + bool is_GDVIRTUAL_CALL_post_process_key_value = true; public: enum AnimationCallbackModeProcess { diff --git a/scene/animation/animation_tree.cpp b/scene/animation/animation_tree.cpp index c5a6a99d07..867bbda4b3 100644 --- a/scene/animation/animation_tree.cpp +++ b/scene/animation/animation_tree.cpp @@ -203,10 +203,11 @@ AnimationNode::NodeTimeInfo AnimationNode::_blend_node(Ref<AnimationNode> p_node } for (const KeyValue<NodePath, bool> &E : filter) { - if (!process_state->track_map.has(E.key)) { + const HashMap<NodePath, int> &map = *process_state->track_map; + if (!map.has(E.key)) { continue; } - int idx = process_state->track_map[E.key]; + int idx = map[E.key]; blendw[idx] = 1.0; // Filtered goes to one. } @@ -618,7 +619,7 @@ bool AnimationTree::_blend_pre_process(double p_delta, int p_track_count, const process_state.valid = true; process_state.invalid_reasons = ""; process_state.last_pass = process_pass; - process_state.track_map = p_track_map; + process_state.track_map = &p_track_map; // Init node state for root AnimationNode. root_animation_node->node_state.track_weights.resize(p_track_count); diff --git a/scene/animation/animation_tree.h b/scene/animation/animation_tree.h index aa497ff1d6..d4b7bf31c9 100644 --- a/scene/animation/animation_tree.h +++ b/scene/animation/animation_tree.h @@ -106,7 +106,7 @@ public: // Temporary state for blending process which needs to be started in the AnimationTree, pass through the AnimationNodes, and then return to the AnimationTree. struct ProcessState { AnimationTree *tree = nullptr; - HashMap<NodePath, int> track_map; // TODO: Is there a better way to manage filter/tracks? + const HashMap<NodePath, int> *track_map; // TODO: Is there a better way to manage filter/tracks? bool is_testing = false; bool valid = false; String invalid_reasons; diff --git a/scene/animation/tween.cpp b/scene/animation/tween.cpp index f8bbd704f4..e1fd8abede 100644 --- a/scene/animation/tween.cpp +++ b/scene/animation/tween.cpp @@ -579,6 +579,7 @@ bool PropertyTweener::step(double &r_delta) { Object *target_instance = ObjectDB::get_instance(target); if (!target_instance) { + _finish(); return false; } elapsed_time += r_delta; @@ -706,6 +707,7 @@ bool CallbackTweener::step(double &r_delta) { } if (!callback.is_valid()) { + _finish(); return false; } @@ -770,6 +772,7 @@ bool MethodTweener::step(double &r_delta) { } if (!callback.is_valid()) { + _finish(); return false; } diff --git a/scene/audio/audio_stream_player.cpp b/scene/audio/audio_stream_player.cpp index 183c4af950..d4b44a8b69 100644 --- a/scene/audio/audio_stream_player.cpp +++ b/scene/audio/audio_stream_player.cpp @@ -154,10 +154,6 @@ void AudioStreamPlayer::_set_playing(bool p_enable) { internal->set_playing(p_enable); } -bool AudioStreamPlayer::_is_active() const { - return internal->is_active(); -} - void AudioStreamPlayer::set_stream_paused(bool p_pause) { internal->set_stream_paused(p_pause); } @@ -249,8 +245,7 @@ void AudioStreamPlayer::_bind_methods() { ClassDB::bind_method(D_METHOD("set_mix_target", "mix_target"), &AudioStreamPlayer::set_mix_target); ClassDB::bind_method(D_METHOD("get_mix_target"), &AudioStreamPlayer::get_mix_target); - ClassDB::bind_method(D_METHOD("_set_playing", "enable"), &AudioStreamPlayer::_set_playing); - ClassDB::bind_method(D_METHOD("_is_active"), &AudioStreamPlayer::_is_active); + ClassDB::bind_method(D_METHOD("set_playing", "enable"), &AudioStreamPlayer::_set_playing); ClassDB::bind_method(D_METHOD("set_stream_paused", "pause"), &AudioStreamPlayer::set_stream_paused); ClassDB::bind_method(D_METHOD("get_stream_paused"), &AudioStreamPlayer::get_stream_paused); @@ -267,7 +262,7 @@ void AudioStreamPlayer::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "stream", PROPERTY_HINT_RESOURCE_TYPE, "AudioStream"), "set_stream", "get_stream"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "volume_db", PROPERTY_HINT_RANGE, "-80,24,suffix:dB"), "set_volume_db", "get_volume_db"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "pitch_scale", PROPERTY_HINT_RANGE, "0.01,4,0.01,or_greater"), "set_pitch_scale", "get_pitch_scale"); - ADD_PROPERTY(PropertyInfo(Variant::BOOL, "playing", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_EDITOR), "_set_playing", "is_playing"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "playing", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_EDITOR), "set_playing", "is_playing"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "autoplay"), "set_autoplay", "is_autoplay_enabled"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "stream_paused", PROPERTY_HINT_NONE, ""), "set_stream_paused", "get_stream_paused"); ADD_PROPERTY(PropertyInfo(Variant::INT, "mix_target", PROPERTY_HINT_ENUM, "Stereo,Surround,Center"), "set_mix_target", "get_mix_target"); diff --git a/scene/gui/base_button.cpp b/scene/gui/base_button.cpp index 01e3cce78b..ed7e0de0e2 100644 --- a/scene/gui/base_button.cpp +++ b/scene/gui/base_button.cpp @@ -140,7 +140,7 @@ void BaseButton::_pressed() { void BaseButton::_toggled(bool p_pressed) { GDVIRTUAL_CALL(_toggled, p_pressed); toggled(p_pressed); - emit_signal(SNAME("toggled"), p_pressed); + emit_signal(SceneStringName(toggled), p_pressed); } void BaseButton::on_action_event(Ref<InputEvent> p_event) { diff --git a/scene/gui/code_edit.cpp b/scene/gui/code_edit.cpp index 412eb83515..e8be38e680 100644 --- a/scene/gui/code_edit.cpp +++ b/scene/gui/code_edit.cpp @@ -211,7 +211,13 @@ void CodeEdit::_notification(int p_what) { tl->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_RIGHT); } else { if (code_completion_options[l].default_value.get_type() == Variant::COLOR) { - draw_rect(Rect2(Point2(code_completion_rect.position.x + code_completion_rect.size.width - icon_area_size.x, icon_area.position.y), icon_area_size), (Color)code_completion_options[l].default_value); + const Color color = code_completion_options[l].default_value; + const Rect2 rect = Rect2(Point2(code_completion_rect.position.x + code_completion_rect.size.width - icon_area_size.x, icon_area.position.y), icon_area_size); + if (color.a < 1.0) { + draw_texture_rect(theme_cache.completion_color_bg, rect, true); + } + + draw_rect(rect, color); } tl->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_LEFT); } @@ -2771,6 +2777,7 @@ void CodeEdit::_bind_methods() { BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_ICON, CodeEdit, can_fold_code_region_icon, "can_fold_code_region"); BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_ICON, CodeEdit, folded_code_region_icon, "folded_code_region"); BIND_THEME_ITEM(Theme::DATA_TYPE_ICON, CodeEdit, folded_eol_icon); + BIND_THEME_ITEM(Theme::DATA_TYPE_ICON, CodeEdit, completion_color_bg); BIND_THEME_ITEM(Theme::DATA_TYPE_COLOR, CodeEdit, breakpoint_color); BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_ICON, CodeEdit, breakpoint_icon, "breakpoint"); diff --git a/scene/gui/code_edit.h b/scene/gui/code_edit.h index 580435f65e..09340be035 100644 --- a/scene/gui/code_edit.h +++ b/scene/gui/code_edit.h @@ -245,6 +245,7 @@ private: Ref<Texture2D> can_fold_code_region_icon; Ref<Texture2D> folded_code_region_icon; Ref<Texture2D> folded_eol_icon; + Ref<Texture2D> completion_color_bg; Color breakpoint_color = Color(1, 1, 1); Ref<Texture2D> breakpoint_icon = Ref<Texture2D>(); diff --git a/scene/gui/color_picker.cpp b/scene/gui/color_picker.cpp index 8a3edc25b9..c92dcbc153 100644 --- a/scene/gui/color_picker.cpp +++ b/scene/gui/color_picker.cpp @@ -783,7 +783,7 @@ void ColorPicker::_add_recent_preset_button(int p_size, const Color &p_color) { recent_preset_hbc->add_child(btn_preset_new); recent_preset_hbc->move_child(btn_preset_new, 0); btn_preset_new->set_pressed(true); - btn_preset_new->connect("toggled", callable_mp(this, &ColorPicker::_recent_preset_pressed).bind(btn_preset_new)); + btn_preset_new->connect(SceneStringName(toggled), callable_mp(this, &ColorPicker::_recent_preset_pressed).bind(btn_preset_new)); } void ColorPicker::_show_hide_preset(const bool &p_is_btn_pressed, Button *p_btn_preset, Container *p_preset_container) { @@ -2003,7 +2003,7 @@ ColorPicker::ColorPicker() { btn_preset->set_toggle_mode(true); btn_preset->set_focus_mode(FOCUS_NONE); btn_preset->set_text_alignment(HORIZONTAL_ALIGNMENT_LEFT); - btn_preset->connect("toggled", callable_mp(this, &ColorPicker::_show_hide_preset).bind(btn_preset, preset_container)); + btn_preset->connect(SceneStringName(toggled), callable_mp(this, &ColorPicker::_show_hide_preset).bind(btn_preset, preset_container)); real_vbox->add_child(btn_preset); real_vbox->add_child(preset_container); @@ -2020,7 +2020,7 @@ ColorPicker::ColorPicker() { btn_recent_preset->set_toggle_mode(true); btn_recent_preset->set_focus_mode(FOCUS_NONE); btn_recent_preset->set_text_alignment(HORIZONTAL_ALIGNMENT_LEFT); - btn_recent_preset->connect("toggled", callable_mp(this, &ColorPicker::_show_hide_preset).bind(btn_recent_preset, recent_preset_hbc)); + btn_recent_preset->connect(SceneStringName(toggled), callable_mp(this, &ColorPicker::_show_hide_preset).bind(btn_recent_preset, recent_preset_hbc)); real_vbox->add_child(btn_recent_preset); real_vbox->add_child(recent_preset_hbc); diff --git a/scene/gui/control.cpp b/scene/gui/control.cpp index b95df86e50..15ada0021a 100644 --- a/scene/gui/control.cpp +++ b/scene/gui/control.cpp @@ -682,12 +682,10 @@ Size2 Control::get_parent_area_size() const { // Positioning and sizing. Transform2D Control::_get_internal_transform() const { - Transform2D rot_scale; - rot_scale.set_rotation_and_scale(data.rotation, data.scale); - Transform2D offset; - offset.set_origin(-data.pivot_offset); - - return offset.affine_inverse() * (rot_scale * offset); + // T(pivot_offset) * R(rotation) * S(scale) * T(-pivot_offset) + Transform2D xform(data.rotation, data.scale, 0.0f, data.pivot_offset); + xform.translate_local(-data.pivot_offset); + return xform; } void Control::_update_canvas_item_transform() { @@ -1748,10 +1746,10 @@ void Control::_size_changed() { // so an up to date global transform could be obtained when handling these. _notify_transform(); + item_rect_changed(size_changed); if (size_changed) { notification(NOTIFICATION_RESIZED); } - item_rect_changed(size_changed); } if (pos_changed && !size_changed) { @@ -3608,7 +3606,7 @@ void Control::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::INT, "focus_mode", PROPERTY_HINT_ENUM, "None,Click,All"), "set_focus_mode", "get_focus_mode"); ADD_GROUP("Mouse", "mouse_"); - ADD_PROPERTY(PropertyInfo(Variant::INT, "mouse_filter", PROPERTY_HINT_ENUM, "Stop,Pass,Ignore"), "set_mouse_filter", "get_mouse_filter"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "mouse_filter", PROPERTY_HINT_ENUM, "Stop,Pass (Propagate Up),Ignore"), "set_mouse_filter", "get_mouse_filter"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "mouse_force_pass_scroll_events"), "set_force_pass_scroll_events", "is_force_pass_scroll_events"); ADD_PROPERTY(PropertyInfo(Variant::INT, "mouse_default_cursor_shape", PROPERTY_HINT_ENUM, "Arrow,I-Beam,Pointing Hand,Cross,Wait,Busy,Drag,Can Drop,Forbidden,Vertical Resize,Horizontal Resize,Secondary Diagonal Resize,Main Diagonal Resize,Move,Vertical Split,Horizontal Split,Help"), "set_default_cursor_shape", "get_default_cursor_shape"); diff --git a/scene/gui/file_dialog.cpp b/scene/gui/file_dialog.cpp index 8047369ab1..1a1aa5ccb0 100644 --- a/scene/gui/file_dialog.cpp +++ b/scene/gui/file_dialog.cpp @@ -1143,7 +1143,7 @@ void FileDialog::_update_option_controls() { CheckBox *cb = memnew(CheckBox); cb->set_pressed(opt.default_idx); grid_options->add_child(cb); - cb->connect("toggled", callable_mp(this, &FileDialog::_option_changed_checkbox_toggled).bind(opt.name)); + cb->connect(SceneStringName(toggled), callable_mp(this, &FileDialog::_option_changed_checkbox_toggled).bind(opt.name)); selected_options[opt.name] = (bool)opt.default_idx; } else { OptionButton *ob = memnew(OptionButton); @@ -1441,7 +1441,7 @@ FileDialog::FileDialog() { show_hidden->set_toggle_mode(true); show_hidden->set_pressed(is_showing_hidden_files()); show_hidden->set_tooltip_text(ETR("Toggle the visibility of hidden files.")); - show_hidden->connect("toggled", callable_mp(this, &FileDialog::set_show_hidden_files)); + show_hidden->connect(SceneStringName(toggled), callable_mp(this, &FileDialog::set_show_hidden_files)); hbc->add_child(show_hidden); shortcuts_container = memnew(HBoxContainer); diff --git a/scene/gui/menu_bar.cpp b/scene/gui/menu_bar.cpp index 3264733548..c8b022d622 100644 --- a/scene/gui/menu_bar.cpp +++ b/scene/gui/menu_bar.cpp @@ -680,10 +680,7 @@ void MenuBar::_bind_methods() { ClassDB::bind_method(D_METHOD("set_menu_hidden", "menu", "hidden"), &MenuBar::set_menu_hidden); ClassDB::bind_method(D_METHOD("is_menu_hidden", "menu"), &MenuBar::is_menu_hidden); - // TODO: Properly handle popups when advanced GUI is disabled. -#ifndef ADVANCED_GUI_DISABLED ClassDB::bind_method(D_METHOD("get_menu_popup", "menu"), &MenuBar::get_menu_popup); -#endif // ADVANCED_GUI_DISABLED ADD_PROPERTY(PropertyInfo(Variant::BOOL, "flat"), "set_flat", "is_flat"); ADD_PROPERTY(PropertyInfo(Variant::INT, "start_index"), "set_start_index", "get_start_index"); diff --git a/scene/gui/menu_button.cpp b/scene/gui/menu_button.cpp index 8c5bb1b33d..1069a752c4 100644 --- a/scene/gui/menu_button.cpp +++ b/scene/gui/menu_button.cpp @@ -173,10 +173,7 @@ bool MenuButton::_get(const StringName &p_name, Variant &r_ret) const { } void MenuButton::_bind_methods() { - // TODO: Properly handle popups when advanced GUI is disabled. -#ifndef ADVANCED_GUI_DISABLED ClassDB::bind_method(D_METHOD("get_popup"), &MenuButton::get_popup); -#endif // ADVANCED_GUI_DISABLED ClassDB::bind_method(D_METHOD("show_popup"), &MenuButton::show_popup); ClassDB::bind_method(D_METHOD("set_switch_on_hover", "enable"), &MenuButton::set_switch_on_hover); ClassDB::bind_method(D_METHOD("is_switch_on_hover"), &MenuButton::is_switch_on_hover); diff --git a/scene/gui/rich_text_label.cpp b/scene/gui/rich_text_label.cpp index e9fe78e162..0395dffad9 100644 --- a/scene/gui/rich_text_label.cpp +++ b/scene/gui/rich_text_label.cpp @@ -817,6 +817,8 @@ int RichTextLabel::_draw_line(ItemFrame *p_frame, int p_line, const Vector2 &p_o } int line_count = 0; + // Bottom margin for text clipping. + float v_limit = theme_cache.normal_style->get_margin(SIDE_BOTTOM); Size2 ctrl_size = get_size(); // Draw text. for (int line = 0; line < l.text_buf->get_line_count(); line++) { @@ -824,7 +826,7 @@ int RichTextLabel::_draw_line(ItemFrame *p_frame, int p_line, const Vector2 &p_o off.y += theme_cache.line_separation; } - if (p_ofs.y + off.y >= ctrl_size.height) { + if (p_ofs.y + off.y >= ctrl_size.height - v_limit) { break; } @@ -1800,13 +1802,13 @@ void RichTextLabel::_notification(int p_what) { case NOTIFICATION_RESIZED: { _stop_thread(); - main->first_resized_line.store(0); //invalidate ALL + main->first_resized_line.store(0); // Invalidate all lines. queue_redraw(); } break; case NOTIFICATION_THEME_CHANGED: { _stop_thread(); - main->first_invalid_font_line.store(0); //invalidate ALL + main->first_invalid_font_line.store(0); // Invalidate all lines. queue_redraw(); } break; @@ -1816,7 +1818,7 @@ void RichTextLabel::_notification(int p_what) { set_text(text); } - main->first_invalid_line.store(0); //invalidate ALL + main->first_invalid_line.store(0); // Invalidate all lines. queue_redraw(); } break; @@ -1890,10 +1892,12 @@ void RichTextLabel::_notification(int p_what) { visible_paragraph_count = 0; visible_line_count = 0; + // Bottom margin for text clipping. + float v_limit = theme_cache.normal_style->get_margin(SIDE_BOTTOM); // New cache draw. Point2 ofs = text_rect.get_position() + Vector2(0, main->lines[from_line].offset.y - vofs); int processed_glyphs = 0; - while (ofs.y < size.height && from_line < to_line) { + while (ofs.y < size.height - v_limit && from_line < to_line) { MutexLock lock(main->lines[from_line].text_buf->get_mutex()); visible_paragraph_count++; @@ -1905,7 +1909,7 @@ void RichTextLabel::_notification(int p_what) { case NOTIFICATION_INTERNAL_PROCESS: { if (is_visible_in_tree()) { - if (!is_ready()) { + if (!is_finished()) { return; } double dt = get_process_delta_time(); @@ -2528,7 +2532,7 @@ PackedFloat32Array RichTextLabel::_find_tab_stops(Item *p_item) { item = item->parent; } - return PackedFloat32Array(); + return default_tab_stops; } HorizontalAlignment RichTextLabel::_find_alignment(Item *p_item) { @@ -2796,7 +2800,7 @@ int RichTextLabel::get_pending_paragraphs() const { return lines - to_line; } -bool RichTextLabel::is_ready() const { +bool RichTextLabel::is_finished() const { const_cast<RichTextLabel *>(this)->_validate_line_caches(); if (updating.load()) { @@ -4444,19 +4448,19 @@ void RichTextLabel::append_text(const String &p_bbcode) { add_text(String::chr(0x00AD)); pos = brk_end + 1; } else if (tag == "center") { - push_paragraph(HORIZONTAL_ALIGNMENT_CENTER); + push_paragraph(HORIZONTAL_ALIGNMENT_CENTER, text_direction, language, st_parser, default_jst_flags, default_tab_stops); pos = brk_end + 1; tag_stack.push_front(tag); } else if (tag == "fill") { - push_paragraph(HORIZONTAL_ALIGNMENT_FILL); + push_paragraph(HORIZONTAL_ALIGNMENT_FILL, text_direction, language, st_parser, default_jst_flags, default_tab_stops); pos = brk_end + 1; tag_stack.push_front(tag); } else if (tag == "left") { - push_paragraph(HORIZONTAL_ALIGNMENT_LEFT); + push_paragraph(HORIZONTAL_ALIGNMENT_LEFT, text_direction, language, st_parser, default_jst_flags, default_tab_stops); pos = brk_end + 1; tag_stack.push_front(tag); } else if (tag == "right") { - push_paragraph(HORIZONTAL_ALIGNMENT_RIGHT); + push_paragraph(HORIZONTAL_ALIGNMENT_RIGHT, text_direction, language, st_parser, default_jst_flags, default_tab_stops); pos = brk_end + 1; tag_stack.push_front(tag); } else if (tag == "ul") { @@ -4515,8 +4519,8 @@ void RichTextLabel::append_text(const String &p_bbcode) { HorizontalAlignment alignment = HORIZONTAL_ALIGNMENT_LEFT; Control::TextDirection dir = Control::TEXT_DIRECTION_INHERITED; - String lang; - PackedFloat32Array tab_stops; + String lang = language; + PackedFloat32Array tab_stops = default_tab_stops; TextServer::StructuredTextParser st_parser_type = TextServer::STRUCTURED_TEXT_DEFAULT; BitField<TextServer::JustificationFlag> jst_flags = default_jst_flags; for (int i = 0; i < subtag.size(); i++) { @@ -5734,19 +5738,89 @@ void RichTextLabel::set_text_direction(Control::TextDirection p_text_direction) if (text_direction != p_text_direction) { text_direction = p_text_direction; - main->first_invalid_line.store(0); //invalidate ALL - _validate_line_caches(); + if (!text.is_empty()) { + _apply_translation(); + } else { + main->first_invalid_line.store(0); // Invalidate all lines. + _validate_line_caches(); + } + queue_redraw(); + } +} + +Control::TextDirection RichTextLabel::get_text_direction() const { + return text_direction; +} + +void RichTextLabel::set_horizontal_alignment(HorizontalAlignment p_alignment) { + ERR_FAIL_INDEX((int)p_alignment, 4); + _stop_thread(); + + if (default_alignment != p_alignment) { + default_alignment = p_alignment; + if (!text.is_empty()) { + _apply_translation(); + } else { + main->first_invalid_line.store(0); // Invalidate all lines. + _validate_line_caches(); + } + queue_redraw(); + } +} + +HorizontalAlignment RichTextLabel::get_horizontal_alignment() const { + return default_alignment; +} + +void RichTextLabel::set_justification_flags(BitField<TextServer::JustificationFlag> p_flags) { + _stop_thread(); + + if (default_jst_flags != p_flags) { + default_jst_flags = p_flags; + if (!text.is_empty()) { + _apply_translation(); + } else { + main->first_invalid_line.store(0); // Invalidate all lines. + _validate_line_caches(); + } queue_redraw(); } } +BitField<TextServer::JustificationFlag> RichTextLabel::get_justification_flags() const { + return default_jst_flags; +} + +void RichTextLabel::set_tab_stops(const PackedFloat32Array &p_tab_stops) { + _stop_thread(); + + if (default_tab_stops != p_tab_stops) { + default_tab_stops = p_tab_stops; + if (!text.is_empty()) { + _apply_translation(); + } else { + main->first_invalid_line.store(0); // Invalidate all lines. + _validate_line_caches(); + } + queue_redraw(); + } +} + +PackedFloat32Array RichTextLabel::get_tab_stops() const { + return default_tab_stops; +} + void RichTextLabel::set_structured_text_bidi_override(TextServer::StructuredTextParser p_parser) { if (st_parser != p_parser) { _stop_thread(); st_parser = p_parser; - main->first_invalid_line.store(0); //invalidate ALL - _validate_line_caches(); + if (!text.is_empty()) { + _apply_translation(); + } else { + main->first_invalid_line.store(0); // Invalidate all lines. + _validate_line_caches(); + } queue_redraw(); } } @@ -5760,7 +5834,7 @@ void RichTextLabel::set_structured_text_bidi_override_options(Array p_args) { _stop_thread(); st_args = p_args; - main->first_invalid_line.store(0); //invalidate ALL + main->first_invalid_line.store(0); // Invalidate all lines. _validate_line_caches(); queue_redraw(); } @@ -5770,17 +5844,17 @@ Array RichTextLabel::get_structured_text_bidi_override_options() const { return st_args; } -Control::TextDirection RichTextLabel::get_text_direction() const { - return text_direction; -} - void RichTextLabel::set_language(const String &p_language) { if (language != p_language) { _stop_thread(); language = p_language; - main->first_invalid_line.store(0); //invalidate ALL - _validate_line_caches(); + if (!text.is_empty()) { + _apply_translation(); + } else { + main->first_invalid_line.store(0); // Invalidate all lines. + _validate_line_caches(); + } queue_redraw(); } } @@ -5794,7 +5868,7 @@ void RichTextLabel::set_autowrap_mode(TextServer::AutowrapMode p_mode) { _stop_thread(); autowrap_mode = p_mode; - main->first_invalid_line = 0; //invalidate ALL + main->first_invalid_line = 0; // Invalidate all lines. _validate_line_caches(); queue_redraw(); } @@ -5820,7 +5894,7 @@ void RichTextLabel::set_visible_ratio(float p_ratio) { } if (visible_chars_behavior == TextServer::VC_CHARS_BEFORE_SHAPING) { - main->first_invalid_line.store(0); // Invalidate ALL. + main->first_invalid_line.store(0); // Invalidate all lines.. _validate_line_caches(); } queue_redraw(); @@ -5948,6 +6022,13 @@ void RichTextLabel::_bind_methods() { ClassDB::bind_method(D_METHOD("set_language", "language"), &RichTextLabel::set_language); ClassDB::bind_method(D_METHOD("get_language"), &RichTextLabel::get_language); + ClassDB::bind_method(D_METHOD("set_horizontal_alignment", "alignment"), &RichTextLabel::set_horizontal_alignment); + ClassDB::bind_method(D_METHOD("get_horizontal_alignment"), &RichTextLabel::get_horizontal_alignment); + ClassDB::bind_method(D_METHOD("set_justification_flags", "justification_flags"), &RichTextLabel::set_justification_flags); + ClassDB::bind_method(D_METHOD("get_justification_flags"), &RichTextLabel::get_justification_flags); + ClassDB::bind_method(D_METHOD("set_tab_stops", "tab_stops"), &RichTextLabel::set_tab_stops); + ClassDB::bind_method(D_METHOD("get_tab_stops"), &RichTextLabel::get_tab_stops); + ClassDB::bind_method(D_METHOD("set_autowrap_mode", "autowrap_mode"), &RichTextLabel::set_autowrap_mode); ClassDB::bind_method(D_METHOD("get_autowrap_mode"), &RichTextLabel::get_autowrap_mode); @@ -6002,7 +6083,10 @@ void RichTextLabel::_bind_methods() { ClassDB::bind_method(D_METHOD("get_text"), &RichTextLabel::get_text); - ClassDB::bind_method(D_METHOD("is_ready"), &RichTextLabel::is_ready); +#ifndef DISABLE_DEPRECATED + ClassDB::bind_method(D_METHOD("is_ready"), &RichTextLabel::is_finished); +#endif // DISABLE_DEPRECATED + ClassDB::bind_method(D_METHOD("is_finished"), &RichTextLabel::is_finished); ClassDB::bind_method(D_METHOD("set_threaded", "threaded"), &RichTextLabel::set_threaded); ClassDB::bind_method(D_METHOD("is_threaded"), &RichTextLabel::is_threaded); @@ -6065,6 +6149,10 @@ void RichTextLabel::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::BOOL, "context_menu_enabled"), "set_context_menu_enabled", "is_context_menu_enabled"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "shortcut_keys_enabled"), "set_shortcut_keys_enabled", "is_shortcut_keys_enabled"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "horizontal_alignment", PROPERTY_HINT_ENUM, "Left,Center,Right,Fill"), "set_horizontal_alignment", "get_horizontal_alignment"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "justification_flags", PROPERTY_HINT_FLAGS, "Kashida Justification:1,Word Justification:2,Justify Only After Last Tab:8,Skip Last Line:32,Skip Last Line With Visible Characters:64,Do Not Skip Single Line:128"), "set_justification_flags", "get_justification_flags"); + ADD_PROPERTY(PropertyInfo(Variant::PACKED_FLOAT32_ARRAY, "tab_stops"), "set_tab_stops", "get_tab_stops"); + ADD_GROUP("Markup", ""); ADD_PROPERTY(PropertyInfo(Variant::ARRAY, "custom_effects", PROPERTY_HINT_ARRAY_TYPE, MAKE_RESOURCE_TYPE_HINT("RichTextEffect"), (PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SCRIPT_VARIABLE)), "set_effects", "get_effects"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "meta_underlined"), "set_meta_underline", "is_meta_underlined"); @@ -6167,7 +6255,7 @@ void RichTextLabel::set_visible_characters_behavior(TextServer::VisibleCharacter _stop_thread(); visible_chars_behavior = p_behavior; - main->first_invalid_line.store(0); //invalidate ALL + main->first_invalid_line.store(0); // Invalidate all lines. _validate_line_caches(); queue_redraw(); } @@ -6187,7 +6275,7 @@ void RichTextLabel::set_visible_characters(int p_visible) { } } if (visible_chars_behavior == TextServer::VC_CHARS_BEFORE_SHAPING) { - main->first_invalid_line.store(0); //invalidate ALL + main->first_invalid_line.store(0); // Invalidate all lines. _validate_line_caches(); } queue_redraw(); diff --git a/scene/gui/rich_text_label.h b/scene/gui/rich_text_label.h index 83285bd7cd..6da13e7b2d 100644 --- a/scene/gui/rich_text_label.h +++ b/scene/gui/rich_text_label.h @@ -482,6 +482,7 @@ private: HorizontalAlignment default_alignment = HORIZONTAL_ALIGNMENT_LEFT; BitField<TextServer::JustificationFlag> default_jst_flags = TextServer::JUSTIFICATION_WORD_BOUND | TextServer::JUSTIFICATION_KASHIDA | TextServer::JUSTIFICATION_SKIP_LAST_LINE | TextServer::JUSTIFICATION_DO_NOT_SKIP_SINGLE_LINE; + PackedFloat32Array default_tab_stops; ItemMeta *meta_hovering = nullptr; Variant current_meta; @@ -785,7 +786,7 @@ public: void deselect(); int get_pending_paragraphs() const; - bool is_ready() const; + bool is_finished() const; bool is_updating() const; void set_threaded(bool p_threaded); @@ -808,6 +809,15 @@ public: void set_text(const String &p_bbcode); String get_text() const; + void set_horizontal_alignment(HorizontalAlignment p_alignment); + HorizontalAlignment get_horizontal_alignment() const; + + void set_justification_flags(BitField<TextServer::JustificationFlag> p_flags); + BitField<TextServer::JustificationFlag> get_justification_flags() const; + + void set_tab_stops(const PackedFloat32Array &p_tab_stops); + PackedFloat32Array get_tab_stops() const; + void set_text_direction(TextDirection p_text_direction); TextDirection get_text_direction() const; diff --git a/scene/gui/spin_box.cpp b/scene/gui/spin_box.cpp index 2c08d36e7e..4212cd709f 100644 --- a/scene/gui/spin_box.cpp +++ b/scene/gui/spin_box.cpp @@ -36,7 +36,7 @@ Size2 SpinBox::get_minimum_size() const { Size2 ms = line_edit->get_combined_minimum_size(); - ms.width += last_w; + ms.width += sizing_cache.buttons_block_width; return ms; } @@ -128,7 +128,7 @@ void SpinBox::_range_click_timeout() { } } -void SpinBox::_release_mouse() { +void SpinBox::_release_mouse_from_drag_mode() { if (drag.enabled) { drag.enabled = false; Input::get_singleton()->set_mouse_mode(Input::MOUSE_MODE_HIDDEN); @@ -137,6 +137,14 @@ void SpinBox::_release_mouse() { } } +void SpinBox::_mouse_exited() { + if (state_cache.up_button_hovered || state_cache.down_button_hovered) { + state_cache.up_button_hovered = false; + state_cache.down_button_hovered = false; + queue_redraw(); + } +} + void SpinBox::gui_input(const Ref<InputEvent> &p_event) { ERR_FAIL_COND(p_event.is_null()); @@ -144,18 +152,36 @@ void SpinBox::gui_input(const Ref<InputEvent> &p_event) { return; } + Ref<InputEventMouse> me = p_event; Ref<InputEventMouseButton> mb = p_event; + Ref<InputEventMouseMotion> mm = p_event; double step = get_custom_arrow_step() != 0.0 ? get_custom_arrow_step() : get_step(); - if (mb.is_valid() && mb->is_pressed()) { - bool up = mb->get_position().y < (get_size().height / 2); + Vector2 mpos; + bool mouse_on_up_button = false; + bool mouse_on_down_button = false; + if (mb.is_valid() || mm.is_valid()) { + Rect2 up_button_rc = Rect2(sizing_cache.buttons_left, 0, sizing_cache.buttons_width, sizing_cache.button_up_height); + Rect2 down_button_rc = Rect2(sizing_cache.buttons_left, sizing_cache.second_button_top, sizing_cache.buttons_width, sizing_cache.button_down_height); + + mpos = me->get_position(); + mouse_on_up_button = up_button_rc.has_point(mpos); + mouse_on_down_button = down_button_rc.has_point(mpos); + } + + if (mb.is_valid() && mb->is_pressed()) { switch (mb->get_button_index()) { case MouseButton::LEFT: { line_edit->grab_focus(); - set_value(get_value() + (up ? step : -step)); + if (mouse_on_up_button || mouse_on_down_button) { + set_value(get_value() + (mouse_on_up_button ? step : -step)); + } + state_cache.up_button_pressed = mouse_on_up_button; + state_cache.down_button_pressed = mouse_on_down_button; + queue_redraw(); range_click_timer->set_wait_time(0.6); range_click_timer->set_one_shot(true); @@ -166,7 +192,9 @@ void SpinBox::gui_input(const Ref<InputEvent> &p_event) { } break; case MouseButton::RIGHT: { line_edit->grab_focus(); - set_value((up ? get_max() : get_min())); + if (mouse_on_up_button || mouse_on_down_button) { + set_value(mouse_on_up_button ? get_max() : get_min()); + } } break; case MouseButton::WHEEL_UP: { if (line_edit->has_focus()) { @@ -186,14 +214,30 @@ void SpinBox::gui_input(const Ref<InputEvent> &p_event) { } if (mb.is_valid() && !mb->is_pressed() && mb->get_button_index() == MouseButton::LEFT) { + if (state_cache.up_button_pressed || state_cache.down_button_pressed) { + state_cache.up_button_pressed = false; + state_cache.down_button_pressed = false; + queue_redraw(); + } + //set_default_cursor_shape(CURSOR_ARROW); range_click_timer->stop(); - _release_mouse(); + _release_mouse_from_drag_mode(); drag.allowed = false; line_edit->clear_pending_select_all_on_focus(); } - Ref<InputEventMouseMotion> mm = p_event; + if (mm.is_valid()) { + bool old_up_hovered = state_cache.up_button_hovered; + bool old_down_hovered = state_cache.down_button_hovered; + + state_cache.up_button_hovered = mouse_on_up_button; + state_cache.down_button_hovered = mouse_on_down_button; + + if (old_up_hovered != state_cache.up_button_hovered || old_down_hovered != state_cache.down_button_hovered) { + queue_redraw(); + } + } if (mm.is_valid() && (mm->get_button_mask().has_flag(MouseButtonMask::LEFT))) { if (drag.enabled) { @@ -239,41 +283,131 @@ void SpinBox::_line_edit_focus_exit() { _text_submitted(line_edit->get_text()); } -inline void SpinBox::_adjust_width_for_icon(const Ref<Texture2D> &icon) { - int w = icon->get_width(); - if ((w != last_w)) { +inline void SpinBox::_compute_sizes() { + int buttons_block_wanted_width = theme_cache.buttons_width + theme_cache.field_and_buttons_separation; + int buttons_block_icon_enforced_width = _get_widest_button_icon_width() + theme_cache.field_and_buttons_separation; + + int w = theme_cache.set_min_buttons_width_from_icons != 0 ? MAX(buttons_block_icon_enforced_width, buttons_block_wanted_width) : buttons_block_wanted_width; + + if (w != sizing_cache.buttons_block_width) { line_edit->set_offset(SIDE_LEFT, 0); line_edit->set_offset(SIDE_RIGHT, -w); - last_w = w; + sizing_cache.buttons_block_width = w; } + + Size2i size = get_size(); + + sizing_cache.buttons_width = w - theme_cache.field_and_buttons_separation; + sizing_cache.buttons_vertical_separation = CLAMP(theme_cache.buttons_vertical_separation, 0, size.height); + sizing_cache.buttons_left = is_layout_rtl() ? 0 : size.width - sizing_cache.buttons_width; + sizing_cache.button_up_height = (size.height - sizing_cache.buttons_vertical_separation) / 2; + sizing_cache.button_down_height = size.height - sizing_cache.button_up_height - sizing_cache.buttons_vertical_separation; + sizing_cache.second_button_top = size.height - sizing_cache.button_down_height; + + sizing_cache.buttons_separator_top = sizing_cache.button_up_height; + sizing_cache.field_and_buttons_separator_left = is_layout_rtl() ? sizing_cache.buttons_width : size.width - sizing_cache.buttons_block_width; + sizing_cache.field_and_buttons_separator_width = theme_cache.field_and_buttons_separation; +} + +inline int SpinBox::_get_widest_button_icon_width() { + int max = 0; + max = MAX(max, theme_cache.updown_icon->get_width()); + max = MAX(max, theme_cache.up_icon->get_width()); + max = MAX(max, theme_cache.up_hover_icon->get_width()); + max = MAX(max, theme_cache.up_pressed_icon->get_width()); + max = MAX(max, theme_cache.up_disabled_icon->get_width()); + max = MAX(max, theme_cache.down_icon->get_width()); + max = MAX(max, theme_cache.down_hover_icon->get_width()); + max = MAX(max, theme_cache.down_pressed_icon->get_width()); + max = MAX(max, theme_cache.down_disabled_icon->get_width()); + return max; } void SpinBox::_notification(int p_what) { switch (p_what) { case NOTIFICATION_DRAW: { _update_text(true); - _adjust_width_for_icon(theme_cache.updown_icon); + _compute_sizes(); RID ci = get_canvas_item(); Size2i size = get_size(); - if (is_layout_rtl()) { - theme_cache.updown_icon->draw(ci, Point2i(0, (size.height - theme_cache.updown_icon->get_height()) / 2)); - } else { - theme_cache.updown_icon->draw(ci, Point2i(size.width - theme_cache.updown_icon->get_width(), (size.height - theme_cache.updown_icon->get_height()) / 2)); + Ref<StyleBox> up_stylebox = theme_cache.up_base_stylebox; + Ref<StyleBox> down_stylebox = theme_cache.down_base_stylebox; + Ref<Texture2D> up_icon = theme_cache.up_icon; + Ref<Texture2D> down_icon = theme_cache.down_icon; + Color up_icon_modulate = theme_cache.up_icon_modulate; + Color down_icon_modulate = theme_cache.down_icon_modulate; + + bool is_fully_disabled = !is_editable(); + + if (state_cache.up_button_disabled || is_fully_disabled) { + up_stylebox = theme_cache.up_disabled_stylebox; + up_icon = theme_cache.up_disabled_icon; + up_icon_modulate = theme_cache.up_disabled_icon_modulate; + } else if (state_cache.up_button_pressed && !drag.enabled) { + up_stylebox = theme_cache.up_pressed_stylebox; + up_icon = theme_cache.up_pressed_icon; + up_icon_modulate = theme_cache.up_pressed_icon_modulate; + } else if (state_cache.up_button_hovered && !drag.enabled) { + up_stylebox = theme_cache.up_hover_stylebox; + up_icon = theme_cache.up_hover_icon; + up_icon_modulate = theme_cache.up_hover_icon_modulate; } + + if (state_cache.down_button_disabled || is_fully_disabled) { + down_stylebox = theme_cache.down_disabled_stylebox; + down_icon = theme_cache.down_disabled_icon; + down_icon_modulate = theme_cache.down_disabled_icon_modulate; + } else if (state_cache.down_button_pressed && !drag.enabled) { + down_stylebox = theme_cache.down_pressed_stylebox; + down_icon = theme_cache.down_pressed_icon; + down_icon_modulate = theme_cache.down_pressed_icon_modulate; + } else if (state_cache.down_button_hovered && !drag.enabled) { + down_stylebox = theme_cache.down_hover_stylebox; + down_icon = theme_cache.down_hover_icon; + down_icon_modulate = theme_cache.down_hover_icon_modulate; + } + + int updown_icon_left = sizing_cache.buttons_left + (sizing_cache.buttons_width - theme_cache.updown_icon->get_width()) / 2; + int updown_icon_top = (size.height - theme_cache.updown_icon->get_height()) / 2; + + // Compute center icon positions once we know which one is used. + int up_icon_left = sizing_cache.buttons_left + (sizing_cache.buttons_width - up_icon->get_width()) / 2; + int up_icon_top = (sizing_cache.button_up_height - up_icon->get_height()) / 2; + int down_icon_left = sizing_cache.buttons_left + (sizing_cache.buttons_width - down_icon->get_width()) / 2; + int down_icon_top = sizing_cache.second_button_top + (sizing_cache.button_down_height - down_icon->get_height()) / 2; + + // Draw separators. + draw_style_box(theme_cache.up_down_buttons_separator, Rect2(sizing_cache.buttons_left, sizing_cache.buttons_separator_top, sizing_cache.buttons_width, sizing_cache.buttons_vertical_separation)); + draw_style_box(theme_cache.field_and_buttons_separator, Rect2(sizing_cache.field_and_buttons_separator_left, 0, sizing_cache.field_and_buttons_separator_width, size.height)); + + // Draw buttons. + draw_style_box(up_stylebox, Rect2(sizing_cache.buttons_left, 0, sizing_cache.buttons_width, sizing_cache.button_up_height)); + draw_style_box(down_stylebox, Rect2(sizing_cache.buttons_left, sizing_cache.second_button_top, sizing_cache.buttons_width, sizing_cache.button_down_height)); + + // Draw arrows. + theme_cache.updown_icon->draw(ci, Point2i(updown_icon_left, updown_icon_top)); + draw_texture(up_icon, Point2i(up_icon_left, up_icon_top), up_icon_modulate); + draw_texture(down_icon, Point2i(down_icon_left, down_icon_top), down_icon_modulate); + + } break; + + case NOTIFICATION_MOUSE_EXIT: { + _mouse_exited(); } break; case NOTIFICATION_ENTER_TREE: { - _adjust_width_for_icon(theme_cache.updown_icon); + _compute_sizes(); _update_text(); + _update_buttons_state_for_current_value(); } break; case NOTIFICATION_VISIBILITY_CHANGED: drag.allowed = false; [[fallthrough]]; case NOTIFICATION_EXIT_TREE: { - _release_mouse(); + _release_mouse_from_drag_mode(); } break; case NOTIFICATION_TRANSLATION_CHANGED: { @@ -353,6 +487,7 @@ bool SpinBox::is_select_all_on_focus() const { void SpinBox::set_editable(bool p_enabled) { line_edit->set_editable(p_enabled); + queue_redraw(); } bool SpinBox::is_editable() const { @@ -371,6 +506,22 @@ double SpinBox::get_custom_arrow_step() const { return custom_arrow_step; } +void SpinBox::_value_changed(double p_value) { + _update_buttons_state_for_current_value(); +} + +void SpinBox::_update_buttons_state_for_current_value() { + double value = get_value(); + bool should_disable_up = value == get_max() && !is_greater_allowed(); + bool should_disable_down = value == get_min() && !is_lesser_allowed(); + + if (state_cache.up_button_disabled != should_disable_up || state_cache.down_button_disabled != should_disable_down) { + state_cache.up_button_disabled = should_disable_up; + state_cache.down_button_disabled = should_disable_down; + queue_redraw(); + } +} + void SpinBox::_bind_methods() { ClassDB::bind_method(D_METHOD("set_horizontal_alignment", "alignment"), &SpinBox::set_horizontal_alignment); ClassDB::bind_method(D_METHOD("get_horizontal_alignment"), &SpinBox::get_horizontal_alignment); @@ -397,13 +548,48 @@ void SpinBox::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "custom_arrow_step", PROPERTY_HINT_RANGE, "0,10000,0.0001,or_greater"), "set_custom_arrow_step", "get_custom_arrow_step"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "select_all_on_focus"), "set_select_all_on_focus", "is_select_all_on_focus"); + BIND_THEME_ITEM(Theme::DATA_TYPE_CONSTANT, SpinBox, buttons_vertical_separation); + BIND_THEME_ITEM(Theme::DATA_TYPE_CONSTANT, SpinBox, field_and_buttons_separation); + BIND_THEME_ITEM(Theme::DATA_TYPE_CONSTANT, SpinBox, buttons_width); + BIND_THEME_ITEM(Theme::DATA_TYPE_CONSTANT, SpinBox, set_min_buttons_width_from_icons); + BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_ICON, SpinBox, updown_icon, "updown"); + BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_ICON, SpinBox, up_icon, "up"); + BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_ICON, SpinBox, up_hover_icon, "up_hover"); + BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_ICON, SpinBox, up_pressed_icon, "up_pressed"); + BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_ICON, SpinBox, up_disabled_icon, "up_disabled"); + BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_ICON, SpinBox, down_icon, "down"); + BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_ICON, SpinBox, down_hover_icon, "down_hover"); + BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_ICON, SpinBox, down_pressed_icon, "down_pressed"); + BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_ICON, SpinBox, down_disabled_icon, "down_disabled"); + + BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_STYLEBOX, SpinBox, up_base_stylebox, "up_background"); + BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_STYLEBOX, SpinBox, up_hover_stylebox, "up_background_hovered"); + BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_STYLEBOX, SpinBox, up_pressed_stylebox, "up_background_pressed"); + BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_STYLEBOX, SpinBox, up_disabled_stylebox, "up_background_disabled"); + BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_STYLEBOX, SpinBox, down_base_stylebox, "down_background"); + BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_STYLEBOX, SpinBox, down_hover_stylebox, "down_background_hovered"); + BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_STYLEBOX, SpinBox, down_pressed_stylebox, "down_background_pressed"); + BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_STYLEBOX, SpinBox, down_disabled_stylebox, "down_background_disabled"); + + BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_COLOR, SpinBox, up_icon_modulate, "up_icon_modulate"); + BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_COLOR, SpinBox, up_hover_icon_modulate, "up_hover_icon_modulate"); + BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_COLOR, SpinBox, up_pressed_icon_modulate, "up_pressed_icon_modulate"); + BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_COLOR, SpinBox, up_disabled_icon_modulate, "up_disabled_icon_modulate"); + BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_COLOR, SpinBox, down_icon_modulate, "down_icon_modulate"); + BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_COLOR, SpinBox, down_hover_icon_modulate, "down_hover_icon_modulate"); + BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_COLOR, SpinBox, down_pressed_icon_modulate, "down_pressed_icon_modulate"); + BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_COLOR, SpinBox, down_disabled_icon_modulate, "down_disabled_icon_modulate"); + + BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_STYLEBOX, SpinBox, field_and_buttons_separator, "field_and_buttons_separator"); + BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_STYLEBOX, SpinBox, up_down_buttons_separator, "up_down_buttons_separator"); } SpinBox::SpinBox() { line_edit = memnew(LineEdit); add_child(line_edit, false, INTERNAL_MODE_FRONT); + line_edit->set_theme_type_variation("SpinBoxInnerLineEdit"); line_edit->set_anchors_and_offsets_preset(Control::PRESET_FULL_RECT); line_edit->set_mouse_filter(MOUSE_FILTER_PASS); line_edit->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_LEFT); diff --git a/scene/gui/spin_box.h b/scene/gui/spin_box.h index 4d49626d71..7c6974f6a8 100644 --- a/scene/gui/spin_box.h +++ b/scene/gui/spin_box.h @@ -39,12 +39,24 @@ class SpinBox : public Range { GDCLASS(SpinBox, Range); LineEdit *line_edit = nullptr; - int last_w = 0; bool update_on_text_changed = false; + struct SizingCache { + int buttons_block_width = 0; + int buttons_width = 0; + int buttons_vertical_separation = 0; + int buttons_left = 0; + int button_up_height = 0; + int button_down_height = 0; + int second_button_top = 0; + int buttons_separator_top = 0; + int field_and_buttons_separator_left = 0; + int field_and_buttons_separator_width = 0; + } sizing_cache; + Timer *range_click_timer = nullptr; void _range_click_timeout(); - void _release_mouse(); + void _release_mouse_from_drag_mode(); void _update_text(bool p_keep_line_edit = false); void _text_submitted(const String &p_string); @@ -65,17 +77,66 @@ class SpinBox : public Range { double diff_y = 0.0; } drag; + struct StateCache { + bool up_button_hovered = false; + bool up_button_pressed = false; + bool up_button_disabled = false; + bool down_button_hovered = false; + bool down_button_pressed = false; + bool down_button_disabled = false; + } state_cache; + void _line_edit_focus_enter(); void _line_edit_focus_exit(); - inline void _adjust_width_for_icon(const Ref<Texture2D> &icon); + inline void _compute_sizes(); + inline int _get_widest_button_icon_width(); struct ThemeCache { Ref<Texture2D> updown_icon; + Ref<Texture2D> up_icon; + Ref<Texture2D> up_hover_icon; + Ref<Texture2D> up_pressed_icon; + Ref<Texture2D> up_disabled_icon; + Ref<Texture2D> down_icon; + Ref<Texture2D> down_hover_icon; + Ref<Texture2D> down_pressed_icon; + Ref<Texture2D> down_disabled_icon; + + Ref<StyleBox> up_base_stylebox; + Ref<StyleBox> up_hover_stylebox; + Ref<StyleBox> up_pressed_stylebox; + Ref<StyleBox> up_disabled_stylebox; + Ref<StyleBox> down_base_stylebox; + Ref<StyleBox> down_hover_stylebox; + Ref<StyleBox> down_pressed_stylebox; + Ref<StyleBox> down_disabled_stylebox; + + Color up_icon_modulate; + Color up_hover_icon_modulate; + Color up_pressed_icon_modulate; + Color up_disabled_icon_modulate; + Color down_icon_modulate; + Color down_hover_icon_modulate; + Color down_pressed_icon_modulate; + Color down_disabled_icon_modulate; + + Ref<StyleBox> field_and_buttons_separator; + Ref<StyleBox> up_down_buttons_separator; + + int buttons_vertical_separation = 0; + int field_and_buttons_separation = 0; + int buttons_width = 0; + int set_min_buttons_width_from_icons = 0; + } theme_cache; + void _mouse_exited(); + void _update_buttons_state_for_current_value(); + protected: virtual void gui_input(const Ref<InputEvent> &p_event) override; + void _value_changed(double p_value) override; void _notification(int p_what); static void _bind_methods(); diff --git a/scene/gui/text_edit.cpp b/scene/gui/text_edit.cpp index 422783b01b..b6835541bf 100644 --- a/scene/gui/text_edit.cpp +++ b/scene/gui/text_edit.cpp @@ -2928,7 +2928,10 @@ void TextEdit::_update_ime_text() { Size2 TextEdit::get_minimum_size() const { Size2 size = theme_cache.style_normal->get_minimum_size(); if (fit_content_height) { - size.y += content_height_cache; + size.y += content_size_cache.y; + } + if (fit_content_width) { + size.x += content_size_cache.x; } return size; } @@ -3098,7 +3101,7 @@ void TextEdit::apply_ime() { insert_text_at_caret(insert_ime_text); } -void TextEdit::set_editable(const bool p_editable) { +void TextEdit::set_editable(bool p_editable) { if (editable == p_editable) { return; } @@ -3223,7 +3226,7 @@ bool TextEdit::is_indent_wrapped_lines() const { } // User controls -void TextEdit::set_overtype_mode_enabled(const bool p_enabled) { +void TextEdit::set_overtype_mode_enabled(bool p_enabled) { if (overtype_mode == p_enabled) { return; } @@ -4473,7 +4476,7 @@ TextEdit::CaretType TextEdit::get_caret_type() const { return caret_type; } -void TextEdit::set_caret_blink_enabled(const bool p_enabled) { +void TextEdit::set_caret_blink_enabled(bool p_enabled) { if (caret_blink_enabled == p_enabled) { return; } @@ -4515,7 +4518,7 @@ bool TextEdit::is_drawing_caret_when_editable_disabled() const { return draw_caret_when_editable_disabled; } -void TextEdit::set_move_caret_on_right_click_enabled(const bool p_enabled) { +void TextEdit::set_move_caret_on_right_click_enabled(bool p_enabled) { move_caret_on_right_click = p_enabled; } @@ -4523,7 +4526,7 @@ bool TextEdit::is_move_caret_on_right_click_enabled() const { return move_caret_on_right_click; } -void TextEdit::set_caret_mid_grapheme_enabled(const bool p_enabled) { +void TextEdit::set_caret_mid_grapheme_enabled(bool p_enabled) { caret_mid_grapheme_enabled = p_enabled; } @@ -4633,7 +4636,7 @@ void TextEdit::add_caret_at_carets(bool p_below) { for (int i = 0; i < num_carets; i++) { const int caret_line = get_caret_line(i); const int caret_column = get_caret_column(i); - const bool is_selected = has_selection(i) || carets[i].last_fit_x != carets[i].selection.origin_last_fit_x; + bool is_selected = has_selection(i) || carets[i].last_fit_x != carets[i].selection.origin_last_fit_x; const int selection_origin_line = get_selection_origin_line(i); const int selection_origin_column = get_selection_origin_column(i); const int caret_wrap_index = get_caret_wrap_index(i); @@ -5098,7 +5101,7 @@ String TextEdit::get_word_under_caret(int p_caret) const { } /* Selection. */ -void TextEdit::set_selecting_enabled(const bool p_enabled) { +void TextEdit::set_selecting_enabled(bool p_enabled) { if (selecting_enabled == p_enabled) { return; } @@ -5114,7 +5117,7 @@ bool TextEdit::is_selecting_enabled() const { return selecting_enabled; } -void TextEdit::set_deselect_on_focus_loss_enabled(const bool p_enabled) { +void TextEdit::set_deselect_on_focus_loss_enabled(bool p_enabled) { if (deselect_on_focus_loss_enabled == p_enabled) { return; } @@ -5129,7 +5132,7 @@ bool TextEdit::is_deselect_on_focus_loss_enabled() const { return deselect_on_focus_loss_enabled; } -void TextEdit::set_drag_and_drop_selection_enabled(const bool p_enabled) { +void TextEdit::set_drag_and_drop_selection_enabled(bool p_enabled) { drag_and_drop_selection_enabled = p_enabled; } @@ -5689,7 +5692,7 @@ Vector<String> TextEdit::get_line_wrapped_text(int p_line) const { /* Viewport */ // Scrolling. -void TextEdit::set_smooth_scroll_enabled(const bool p_enabled) { +void TextEdit::set_smooth_scroll_enabled(bool p_enabled) { v_scroll->set_smooth_scroll_enabled(p_enabled); smooth_scroll_enabled = p_enabled; } @@ -5698,7 +5701,7 @@ bool TextEdit::is_smooth_scroll_enabled() const { return smooth_scroll_enabled; } -void TextEdit::set_scroll_past_end_of_file_enabled(const bool p_enabled) { +void TextEdit::set_scroll_past_end_of_file_enabled(bool p_enabled) { if (scroll_past_end_of_file_enabled == p_enabled) { return; } @@ -5752,7 +5755,7 @@ float TextEdit::get_v_scroll_speed() const { return v_scroll_speed; } -void TextEdit::set_fit_content_height_enabled(const bool p_enabled) { +void TextEdit::set_fit_content_height_enabled(bool p_enabled) { if (fit_content_height == p_enabled) { return; } @@ -5764,6 +5767,18 @@ bool TextEdit::is_fit_content_height_enabled() const { return fit_content_height; } +void TextEdit::set_fit_content_width_enabled(bool p_enabled) { + if (fit_content_width == p_enabled) { + return; + } + fit_content_width = p_enabled; + update_minimum_size(); +} + +bool TextEdit::is_fit_content_width_enabled() const { + return fit_content_width; +} + double TextEdit::get_scroll_pos_for_line(int p_line, int p_wrap_index) const { ERR_FAIL_INDEX_V(p_line, text.size(), 0); ERR_FAIL_COND_V(p_wrap_index < 0, 0); @@ -6317,7 +6332,7 @@ bool TextEdit::is_highlight_current_line_enabled() const { return highlight_current_line; } -void TextEdit::set_highlight_all_occurrences(const bool p_enabled) { +void TextEdit::set_highlight_all_occurrences(bool p_enabled) { if (highlight_all_occurrences == p_enabled) { return; } @@ -6735,6 +6750,9 @@ void TextEdit::_bind_methods() { ClassDB::bind_method(D_METHOD("set_fit_content_height_enabled", "enabled"), &TextEdit::set_fit_content_height_enabled); ClassDB::bind_method(D_METHOD("is_fit_content_height_enabled"), &TextEdit::is_fit_content_height_enabled); + ClassDB::bind_method(D_METHOD("set_fit_content_width_enabled", "enabled"), &TextEdit::set_fit_content_width_enabled); + ClassDB::bind_method(D_METHOD("is_fit_content_width_enabled"), &TextEdit::is_fit_content_width_enabled); + ClassDB::bind_method(D_METHOD("get_scroll_pos_for_line", "line", "wrap_index"), &TextEdit::get_scroll_pos_for_line, DEFVAL(0)); // Visible lines. @@ -6859,6 +6877,7 @@ void TextEdit::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "scroll_vertical", PROPERTY_HINT_NONE, "suffix:lines"), "set_v_scroll", "get_v_scroll"); ADD_PROPERTY(PropertyInfo(Variant::INT, "scroll_horizontal", PROPERTY_HINT_NONE, "suffix:px"), "set_h_scroll", "get_h_scroll"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "scroll_fit_content_height"), "set_fit_content_height_enabled", "is_fit_content_height_enabled"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "scroll_fit_content_width"), "set_fit_content_width_enabled", "is_fit_content_width_enabled"); ADD_GROUP("Minimap", "minimap_"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "minimap_draw"), "set_draw_minimap", "is_drawing_minimap"); @@ -7846,8 +7865,8 @@ void TextEdit::_update_scrollbars() { total_width += minimap_width; } - content_height_cache = MAX(total_rows, 1) * get_line_height(); - if (fit_content_height) { + content_size_cache = Vector2i(total_width + 10, MAX(total_rows, 1) * get_line_height()); + if (fit_content_height || fit_content_width) { update_minimum_size(); } @@ -8054,7 +8073,7 @@ void TextEdit::_update_minimap_hover() { const Point2 mp = get_local_mouse_pos(); const int xmargin_end = get_size().width - theme_cache.style_normal->get_margin(SIDE_RIGHT); - const bool hovering_sidebar = mp.x > xmargin_end - minimap_width && mp.x < xmargin_end; + bool hovering_sidebar = mp.x > xmargin_end - minimap_width && mp.x < xmargin_end; if (!hovering_sidebar) { if (hovering_minimap) { // Only redraw if the hovering status changed. @@ -8068,7 +8087,7 @@ void TextEdit::_update_minimap_hover() { const int row = get_minimap_line_at_pos(mp); - const bool new_hovering_minimap = row >= get_first_visible_line() && row <= get_last_full_visible_line(); + bool new_hovering_minimap = row >= get_first_visible_line() && row <= get_last_full_visible_line(); if (new_hovering_minimap != hovering_minimap) { // Only redraw if the hovering status changed. hovering_minimap = new_hovering_minimap; diff --git a/scene/gui/text_edit.h b/scene/gui/text_edit.h index c8cd7b0e4d..1f2fd6619a 100644 --- a/scene/gui/text_edit.h +++ b/scene/gui/text_edit.h @@ -505,8 +505,9 @@ private: HScrollBar *h_scroll = nullptr; VScrollBar *v_scroll = nullptr; - float content_height_cache = 0.0; + Vector2i content_size_cache; bool fit_content_height = false; + bool fit_content_width = false; bool scroll_past_end_of_file_enabled = false; // Smooth scrolling. @@ -734,7 +735,7 @@ public: void cancel_ime(); void apply_ime(); - void set_editable(const bool p_editable); + void set_editable(bool p_editable); bool is_editable() const; void set_text_direction(TextDirection p_text_direction); @@ -755,7 +756,7 @@ public: bool is_indent_wrapped_lines() const; // User controls - void set_overtype_mode_enabled(const bool p_enabled); + void set_overtype_mode_enabled(bool p_enabled); bool is_overtype_mode_enabled() const; void set_context_menu_enabled(bool p_enabled); @@ -862,7 +863,7 @@ public: void set_caret_type(CaretType p_type); CaretType get_caret_type() const; - void set_caret_blink_enabled(const bool p_enabled); + void set_caret_blink_enabled(bool p_enabled); bool is_caret_blink_enabled() const; void set_caret_blink_interval(const float p_interval); @@ -871,10 +872,10 @@ public: void set_draw_caret_when_editable_disabled(bool p_enable); bool is_drawing_caret_when_editable_disabled() const; - void set_move_caret_on_right_click_enabled(const bool p_enabled); + void set_move_caret_on_right_click_enabled(bool p_enabled); bool is_move_caret_on_right_click_enabled() const; - void set_caret_mid_grapheme_enabled(const bool p_enabled); + void set_caret_mid_grapheme_enabled(bool p_enabled); bool is_caret_mid_grapheme_enabled() const; void set_multiple_carets_enabled(bool p_enabled); @@ -910,13 +911,13 @@ public: String get_word_under_caret(int p_caret = -1) const; /* Selection. */ - void set_selecting_enabled(const bool p_enabled); + void set_selecting_enabled(bool p_enabled); bool is_selecting_enabled() const; - void set_deselect_on_focus_loss_enabled(const bool p_enabled); + void set_deselect_on_focus_loss_enabled(bool p_enabled); bool is_deselect_on_focus_loss_enabled() const; - void set_drag_and_drop_selection_enabled(const bool p_enabled); + void set_drag_and_drop_selection_enabled(bool p_enabled); bool is_drag_and_drop_selection_enabled() const; void set_selection_mode(SelectionMode p_mode); @@ -965,10 +966,10 @@ public: /* Viewport. */ // Scrolling. - void set_smooth_scroll_enabled(const bool p_enabled); + void set_smooth_scroll_enabled(bool p_enabled); bool is_smooth_scroll_enabled() const; - void set_scroll_past_end_of_file_enabled(const bool p_enabled); + void set_scroll_past_end_of_file_enabled(bool p_enabled); bool is_scroll_past_end_of_file_enabled() const; VScrollBar *get_v_scroll_bar() const; @@ -983,9 +984,12 @@ public: void set_v_scroll_speed(float p_speed); float get_v_scroll_speed() const; - void set_fit_content_height_enabled(const bool p_enabled); + void set_fit_content_height_enabled(bool p_enabled); bool is_fit_content_height_enabled() const; + void set_fit_content_width_enabled(bool p_enabled); + bool is_fit_content_width_enabled() const; + double get_scroll_pos_for_line(int p_line, int p_wrap_index = 0) const; // Visible lines. @@ -1071,7 +1075,7 @@ public: void set_highlight_current_line(bool p_enabled); bool is_highlight_current_line_enabled() const; - void set_highlight_all_occurrences(const bool p_enabled); + void set_highlight_all_occurrences(bool p_enabled); bool is_highlight_all_occurrences_enabled() const; void set_draw_control_chars(bool p_enabled); diff --git a/scene/gui/tree.cpp b/scene/gui/tree.cpp index 46fcdcf7f6..7097fe0215 100644 --- a/scene/gui/tree.cpp +++ b/scene/gui/tree.cpp @@ -773,17 +773,21 @@ TreeItem *TreeItem::create_child(int p_index) { TreeItem *item_prev = nullptr; TreeItem *item_next = first_child; - int idx = 0; - while (item_next) { - if (idx == p_index) { - item_next->prev = ti; - ti->next = item_next; - break; - } + if (p_index < 0 && last_child) { + item_prev = last_child; + } else { + int idx = 0; + while (item_next) { + if (idx == p_index) { + item_next->prev = ti; + ti->next = item_next; + break; + } - item_prev = item_next; - item_next = item_next->next; - idx++; + item_prev = item_next; + item_next = item_next->next; + idx++; + } } if (item_prev) { @@ -804,6 +808,10 @@ TreeItem *TreeItem::create_child(int p_index) { } } + if (item_prev == last_child) { + last_child = ti; + } + ti->parent = this; ti->parent_visible_in_tree = is_visible_in_tree(); @@ -820,17 +828,13 @@ void TreeItem::add_child(TreeItem *p_item) { p_item->parent_visible_in_tree = is_visible_in_tree(); p_item->_handle_visibility_changed(p_item->parent_visible_in_tree); - TreeItem *item_prev = first_child; - while (item_prev && item_prev->next) { - item_prev = item_prev->next; - } - - if (item_prev) { - item_prev->next = p_item; - p_item->prev = item_prev; + if (last_child) { + last_child->next = p_item; + p_item->prev = last_child; } else { first_child = p_item; } + last_child = p_item; if (!children_cache.is_empty()) { children_cache.append(p_item); @@ -910,13 +914,8 @@ TreeItem *TreeItem::_get_prev_in_tree(bool p_wrap, bool p_include_invisible) { } } else { current = prev_item; - while ((!current->collapsed || p_include_invisible) && current->first_child) { - //go to the very end - - current = current->first_child; - while (current->next) { - current = current->next; - } + while ((!current->collapsed || p_include_invisible) && current->last_child) { + current = current->last_child; } } @@ -1037,6 +1036,8 @@ void TreeItem::clear_children() { } first_child = nullptr; + last_child = nullptr; + children_cache.clear(); }; int TreeItem::get_index() { @@ -1141,6 +1142,7 @@ void TreeItem::move_after(TreeItem *p_item) { if (next) { parent->children_cache.clear(); } else { + parent->last_child = this; // If the cache is empty, it has not been built but there // are items in the tree (note p_item != nullptr,) so we cannot update it. if (!parent->children_cache.is_empty()) { @@ -3272,12 +3274,10 @@ void Tree::value_editor_changed(double p_value) { return; } - TreeItem::Cell &c = popup_edited_item->cells.write[popup_edited_item_col]; - c.val = p_value; + const TreeItem::Cell &c = popup_edited_item->cells[popup_edited_item_col]; - line_editor->set_text(String::num(c.val, Math::range_step_decimals(c.step))); + line_editor->set_text(String::num(p_value, Math::range_step_decimals(c.step))); - item_edited(popup_edited_item_col, popup_edited_item); queue_redraw(); } @@ -4468,15 +4468,8 @@ TreeItem *Tree::get_root() const { TreeItem *Tree::get_last_item() const { TreeItem *last = root; - - while (last) { - if (last->next) { - last = last->next; - } else if (last->first_child && !last->collapsed) { - last = last->first_child; - } else { - break; - } + while (last && last->last_child && !last->collapsed) { + last = last->last_child; } return last; @@ -4495,9 +4488,16 @@ void Tree::item_edited(int p_column, TreeItem *p_item, MouseButton p_custom_mous } void Tree::item_changed(int p_column, TreeItem *p_item) { - if (p_item != nullptr && p_column >= 0 && p_column < p_item->cells.size()) { - p_item->cells.write[p_column].dirty = true; - columns.write[p_column].cached_minimum_width_dirty = true; + if (p_item != nullptr) { + if (p_column >= 0 && p_column < p_item->cells.size()) { + p_item->cells.write[p_column].dirty = true; + columns.write[p_column].cached_minimum_width_dirty = true; + } else if (p_column == -1) { + for (int i = 0; i < p_item->cells.size(); i++) { + p_item->cells.write[i].dirty = true; + columns.write[i].cached_minimum_width_dirty = true; + } + } } queue_redraw(); } diff --git a/scene/gui/tree.h b/scene/gui/tree.h index 3200459b5a..4518708685 100644 --- a/scene/gui/tree.h +++ b/scene/gui/tree.h @@ -136,6 +136,7 @@ private: TreeItem *prev = nullptr; // previous in list TreeItem *next = nullptr; // next in list TreeItem *first_child = nullptr; + TreeItem *last_child = nullptr; Vector<TreeItem *> children_cache; bool is_root = false; // for tree root @@ -177,6 +178,9 @@ private: if (parent->first_child == this) { parent->first_child = next; } + if (parent->last_child == this) { + parent->last_child = prev; + } } } diff --git a/scene/main/node.h b/scene/main/node.h index 2f6372dad5..e412459105 100644 --- a/scene/main/node.h +++ b/scene/main/node.h @@ -657,7 +657,7 @@ public: return binds; } - void replace_by(Node *p_node, bool p_keep_data = false); + void replace_by(Node *p_node, bool p_keep_groups = false); void set_process_mode(ProcessMode p_mode); ProcessMode get_process_mode() const; diff --git a/scene/main/viewport.cpp b/scene/main/viewport.cpp index 0036247625..a370ef2d18 100644 --- a/scene/main/viewport.cpp +++ b/scene/main/viewport.cpp @@ -884,6 +884,10 @@ void Viewport::_process_picking() { } #ifndef _3D_DISABLED + if (physics_object_picking_first_only && is_input_handled()) { + continue; + } + CollisionObject3D *capture_object = nullptr; if (physics_object_capture.is_valid()) { capture_object = Object::cast_to<CollisionObject3D>(ObjectDB::get_instance(physics_object_capture)); @@ -1933,7 +1937,12 @@ void Viewport::_gui_input_event(Ref<InputEvent> p_event) { } } - if (!is_tooltip_shown && over->can_process()) { + // If the tooltip timer isn't running, start it. + // Otherwise, only reset the timer if the mouse has moved more than 5 pixels. + if (!is_tooltip_shown && over->can_process() && + (gui.tooltip_timer.is_null() || + Math::is_zero_approx(gui.tooltip_timer->get_time_left()) || + mm->get_relative().length() > 5.0)) { if (gui.tooltip_timer.is_valid()) { gui.tooltip_timer->release_connections(); gui.tooltip_timer = Ref<SceneTreeTimer>(); diff --git a/scene/register_scene_types.cpp b/scene/register_scene_types.cpp index aa8ff75c6a..76678e609a 100644 --- a/scene/register_scene_types.cpp +++ b/scene/register_scene_types.cpp @@ -402,8 +402,6 @@ void register_scene_types() { GDREGISTER_CLASS(VSlider); GDREGISTER_CLASS(Popup); GDREGISTER_CLASS(PopupPanel); - GDREGISTER_CLASS(MenuBar); - GDREGISTER_CLASS(MenuButton); GDREGISTER_CLASS(CheckBox); GDREGISTER_CLASS(CheckButton); GDREGISTER_CLASS(LinkButton); @@ -458,6 +456,8 @@ void register_scene_types() { GDREGISTER_CLASS(CodeHighlighter); GDREGISTER_ABSTRACT_CLASS(TreeItem); + GDREGISTER_CLASS(MenuBar); + GDREGISTER_CLASS(MenuButton); GDREGISTER_CLASS(OptionButton); GDREGISTER_CLASS(SpinBox); GDREGISTER_CLASS(ColorPicker); diff --git a/scene/resources/2d/navigation_polygon.h b/scene/resources/2d/navigation_polygon.h index 86bda47ace..ed2c606c55 100644 --- a/scene/resources/2d/navigation_polygon.h +++ b/scene/resources/2d/navigation_polygon.h @@ -33,6 +33,7 @@ #include "scene/2d/node_2d.h" #include "scene/resources/navigation_mesh.h" +#include "servers/navigation/navigation_globals.h" class NavigationPolygon : public Resource { GDCLASS(NavigationPolygon, Resource); @@ -50,7 +51,7 @@ class NavigationPolygon : public Resource { // Navigation mesh Ref<NavigationMesh> navigation_mesh; - real_t cell_size = 1.0f; // Must match ProjectSettings default 2D cell_size. + real_t cell_size = NavigationDefaults2D::navmesh_cell_size; real_t border_size = 0.0f; Rect2 baking_rect; diff --git a/scene/resources/3d/fog_material.cpp b/scene/resources/3d/fog_material.cpp index 5e4f1970ee..92246b50db 100644 --- a/scene/resources/3d/fog_material.cpp +++ b/scene/resources/3d/fog_material.cpp @@ -138,7 +138,7 @@ void FogMaterial::cleanup_shader() { } void FogMaterial::_update_shader() { - shader_mutex.lock(); + MutexLock shader_lock(shader_mutex); if (shader.is_null()) { shader = RS::get_singleton()->shader_create(); @@ -165,7 +165,6 @@ void fog() { } )"); } - shader_mutex.unlock(); } FogMaterial::FogMaterial() { diff --git a/scene/resources/3d/importer_mesh.cpp b/scene/resources/3d/importer_mesh.cpp index b0633c06b9..4f4c485db3 100644 --- a/scene/resources/3d/importer_mesh.cpp +++ b/scene/resources/3d/importer_mesh.cpp @@ -1418,5 +1418,5 @@ void ImporterMesh::_bind_methods() { ClassDB::bind_method(D_METHOD("set_lightmap_size_hint", "size"), &ImporterMesh::set_lightmap_size_hint); ClassDB::bind_method(D_METHOD("get_lightmap_size_hint"), &ImporterMesh::get_lightmap_size_hint); - ADD_PROPERTY(PropertyInfo(Variant::DICTIONARY, "_data", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR), "_set_data", "_get_data"); + ADD_PROPERTY(PropertyInfo(Variant::DICTIONARY, "_data", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR | PROPERTY_USAGE_INTERNAL), "_set_data", "_get_data"); } diff --git a/scene/resources/3d/sky_material.cpp b/scene/resources/3d/sky_material.cpp index 640261d615..c470db5d7f 100644 --- a/scene/resources/3d/sky_material.cpp +++ b/scene/resources/3d/sky_material.cpp @@ -269,7 +269,7 @@ void ProceduralSkyMaterial::cleanup_shader() { } void ProceduralSkyMaterial::_update_shader() { - shader_mutex.lock(); + MutexLock shader_lock(shader_mutex); if (shader_cache[0].is_null()) { for (int i = 0; i < 2; i++) { shader_cache[i] = RS::get_singleton()->shader_create(); @@ -354,7 +354,6 @@ void sky() { i ? "render_mode use_debanding;" : "")); } } - shader_mutex.unlock(); } ProceduralSkyMaterial::ProceduralSkyMaterial() { @@ -463,7 +462,7 @@ void PanoramaSkyMaterial::cleanup_shader() { } void PanoramaSkyMaterial::_update_shader() { - shader_mutex.lock(); + MutexLock shader_lock(shader_mutex); if (shader_cache[0].is_null()) { for (int i = 0; i < 2; i++) { shader_cache[i] = RS::get_singleton()->shader_create(); @@ -484,8 +483,6 @@ void sky() { i ? "filter_linear" : "filter_nearest")); } } - - shader_mutex.unlock(); } PanoramaSkyMaterial::PanoramaSkyMaterial() { @@ -692,7 +689,7 @@ void PhysicalSkyMaterial::cleanup_shader() { } void PhysicalSkyMaterial::_update_shader() { - shader_mutex.lock(); + MutexLock shader_lock(shader_mutex); if (shader_cache[0].is_null()) { for (int i = 0; i < 2; i++) { shader_cache[i] = RS::get_singleton()->shader_create(); @@ -785,8 +782,6 @@ void sky() { i ? "render_mode use_debanding;" : "")); } } - - shader_mutex.unlock(); } PhysicalSkyMaterial::PhysicalSkyMaterial() { diff --git a/scene/resources/animation.h b/scene/resources/animation.h index 9e6d34959a..0c29790ea4 100644 --- a/scene/resources/animation.h +++ b/scene/resources/animation.h @@ -45,7 +45,7 @@ public: static inline String PARAMETERS_BASE_PATH = "parameters/"; - enum TrackType { + enum TrackType : uint8_t { TYPE_VALUE, // Set a value in a property, can be interpolated. TYPE_POSITION_3D, // Position 3D track, can be compressed. TYPE_ROTATION_3D, // Rotation 3D track, can be compressed. @@ -57,7 +57,7 @@ public: TYPE_ANIMATION, }; - enum InterpolationType { + enum InterpolationType : uint8_t { INTERPOLATION_NEAREST, INTERPOLATION_LINEAR, INTERPOLATION_CUBIC, @@ -65,26 +65,26 @@ public: INTERPOLATION_CUBIC_ANGLE, }; - enum UpdateMode { + enum UpdateMode : uint8_t { UPDATE_CONTINUOUS, UPDATE_DISCRETE, UPDATE_CAPTURE, }; - enum LoopMode { + enum LoopMode : uint8_t { LOOP_NONE, LOOP_LINEAR, LOOP_PINGPONG, }; // LoopedFlag is used in Animataion to "process the keys at both ends correct". - enum LoopedFlag { + enum LoopedFlag : uint8_t { LOOPED_FLAG_NONE, LOOPED_FLAG_END, LOOPED_FLAG_START, }; - enum FindMode { + enum FindMode : uint8_t { FIND_MODE_NEAREST, FIND_MODE_APPROX, FIND_MODE_EXACT, @@ -104,7 +104,6 @@ public: }; #endif // TOOLS_ENABLED -private: struct Track { TrackType type = TrackType::TYPE_ANIMATION; InterpolationType interpolation = INTERPOLATION_LINEAR; @@ -117,6 +116,7 @@ private: virtual ~Track() {} }; +private: struct Key { real_t transition = 1.0; double time = 0.0; // Time in secs. @@ -396,6 +396,10 @@ public: int add_track(TrackType p_type, int p_at_pos = -1); void remove_track(int p_track); + _FORCE_INLINE_ const Vector<Track *> get_tracks() { + return tracks; + } + bool is_capture_included() const; int get_track_count() const; diff --git a/scene/resources/audio_stream_wav.cpp b/scene/resources/audio_stream_wav.cpp index de6a069567..08ebacc2b3 100644 --- a/scene/resources/audio_stream_wav.cpp +++ b/scene/resources/audio_stream_wav.cpp @@ -179,17 +179,17 @@ void AudioStreamPlaybackWAV::do_resample(const Depth *p_src, AudioFrame *p_dst, if (pos != p_qoa->cache_pos) { // Prevents triple decoding on lower mix rates. for (int i = 0; i < 2; i++) { // Sign operations prevent triple decoding on backward loops, maxing prevents pop. - uint32_t interp_pos = MIN(pos + (i * sign) + (sign < 0), p_qoa->desc->samples - 1); + uint32_t interp_pos = MIN(pos + (i * sign) + (sign < 0), p_qoa->desc.samples - 1); uint32_t new_data_ofs = 8 + interp_pos / QOA_FRAME_LEN * p_qoa->frame_len; if (p_qoa->data_ofs != new_data_ofs) { p_qoa->data_ofs = new_data_ofs; const uint8_t *src_ptr = (const uint8_t *)base->data; src_ptr += p_qoa->data_ofs + AudioStreamWAV::DATA_PAD; - qoa_decode_frame(src_ptr, p_qoa->frame_len, p_qoa->desc, p_qoa->dec, &p_qoa->dec_len); + qoa_decode_frame(src_ptr, p_qoa->frame_len, &p_qoa->desc, p_qoa->dec, &p_qoa->dec_len); } - uint32_t dec_idx = (interp_pos % QOA_FRAME_LEN) * p_qoa->desc->channels; + uint32_t dec_idx = (interp_pos % QOA_FRAME_LEN) * p_qoa->desc.channels; if ((sign > 0 && i == 0) || (sign < 0 && i == 1)) { final = p_qoa->dec[dec_idx]; @@ -286,7 +286,7 @@ int AudioStreamPlaybackWAV::mix(AudioFrame *p_buffer, float p_rate_scale, int p_ len *= 2; break; case AudioStreamWAV::FORMAT_QOA: - len = qoa.desc->samples * qoa.desc->channels; + len = qoa.desc.samples * qoa.desc.channels; break; } @@ -484,10 +484,6 @@ void AudioStreamPlaybackWAV::set_sample_playback(const Ref<AudioSamplePlayback> AudioStreamPlaybackWAV::AudioStreamPlaybackWAV() {} AudioStreamPlaybackWAV::~AudioStreamPlaybackWAV() { - if (qoa.desc) { - memfree(qoa.desc); - } - if (qoa.dec) { memfree(qoa.dec); } @@ -557,7 +553,7 @@ double AudioStreamWAV::get_length() const { len *= 2; break; case AudioStreamWAV::FORMAT_QOA: - qoa_desc desc = { 0, 0, 0, { { { 0 }, { 0 } } } }; + qoa_desc desc = {}; qoa_decode_header((uint8_t *)data + DATA_PAD, data_bytes, &desc); len = desc.samples * desc.channels; break; @@ -697,12 +693,11 @@ Ref<AudioStreamPlayback> AudioStreamWAV::instantiate_playback() { sample->base = Ref<AudioStreamWAV>(this); if (format == AudioStreamWAV::FORMAT_QOA) { - sample->qoa.desc = (qoa_desc *)memalloc(sizeof(qoa_desc)); - uint32_t ffp = qoa_decode_header((uint8_t *)data + DATA_PAD, data_bytes, sample->qoa.desc); + uint32_t ffp = qoa_decode_header((uint8_t *)data + DATA_PAD, data_bytes, &sample->qoa.desc); ERR_FAIL_COND_V(ffp != 8, Ref<AudioStreamPlaybackWAV>()); - sample->qoa.frame_len = qoa_max_frame_size(sample->qoa.desc); - int samples_len = (sample->qoa.desc->samples > QOA_FRAME_LEN ? QOA_FRAME_LEN : sample->qoa.desc->samples); - int alloc_len = sample->qoa.desc->channels * samples_len * sizeof(int16_t); + sample->qoa.frame_len = qoa_max_frame_size(&sample->qoa.desc); + int samples_len = (sample->qoa.desc.samples > QOA_FRAME_LEN ? QOA_FRAME_LEN : sample->qoa.desc.samples); + int alloc_len = sample->qoa.desc.channels * samples_len * sizeof(int16_t); sample->qoa.dec = (int16_t *)memalloc(alloc_len); } @@ -765,7 +760,7 @@ void AudioStreamWAV::_bind_methods() { ClassDB::bind_method(D_METHOD("save_to_wav", "path"), &AudioStreamWAV::save_to_wav); ADD_PROPERTY(PropertyInfo(Variant::PACKED_BYTE_ARRAY, "data", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR), "set_data", "get_data"); - ADD_PROPERTY(PropertyInfo(Variant::INT, "format", PROPERTY_HINT_ENUM, "8-Bit,16-Bit,IMA-ADPCM,QOA"), "set_format", "get_format"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "format", PROPERTY_HINT_ENUM, "8-Bit,16-Bit,IMA ADPCM,Quite OK Audio"), "set_format", "get_format"); ADD_PROPERTY(PropertyInfo(Variant::INT, "loop_mode", PROPERTY_HINT_ENUM, "Disabled,Forward,Ping-Pong,Backward"), "set_loop_mode", "get_loop_mode"); ADD_PROPERTY(PropertyInfo(Variant::INT, "loop_begin"), "set_loop_begin", "get_loop_begin"); ADD_PROPERTY(PropertyInfo(Variant::INT, "loop_end"), "set_loop_end", "get_loop_end"); diff --git a/scene/resources/audio_stream_wav.h b/scene/resources/audio_stream_wav.h index 806db675b6..47aa10e790 100644 --- a/scene/resources/audio_stream_wav.h +++ b/scene/resources/audio_stream_wav.h @@ -59,7 +59,7 @@ class AudioStreamPlaybackWAV : public AudioStreamPlayback { } ima_adpcm[2]; struct QOA_State { - qoa_desc *desc = nullptr; + qoa_desc desc = {}; uint32_t data_ofs = 0; uint32_t frame_len = 0; int16_t *dec = nullptr; diff --git a/scene/resources/material.cpp b/scene/resources/material.cpp index 7d121c9d87..d07dee6674 100644 --- a/scene/resources/material.cpp +++ b/scene/resources/material.cpp @@ -379,6 +379,8 @@ bool ShaderMaterial::_property_can_revert(const StringName &p_name) const { Variant default_value = RenderingServer::get_singleton()->shader_get_parameter_default(shader->get_rid(), *pr); Variant current_value = get_shader_parameter(*pr); return default_value.get_type() != Variant::NIL && default_value != current_value; + } else if (p_name == "render_priority" || p_name == "next_pass") { + return true; } } return false; @@ -390,6 +392,12 @@ bool ShaderMaterial::_property_get_revert(const StringName &p_name, Variant &r_p if (pr) { r_property = RenderingServer::get_singleton()->shader_get_parameter_default(shader->get_rid(), *pr); return true; + } else if (p_name == "render_priority") { + r_property = 0; + return true; + } else if (p_name == "next_pass") { + r_property = Variant(); + return true; } } return false; diff --git a/scene/resources/mesh.cpp b/scene/resources/mesh.cpp index 8b5e438aea..22e2e9138f 100644 --- a/scene/resources/mesh.cpp +++ b/scene/resources/mesh.cpp @@ -2251,6 +2251,7 @@ Error ArrayMesh::lightmap_unwrap_cached(const Transform3D &p_base_transform, flo } void ArrayMesh::set_shadow_mesh(const Ref<ArrayMesh> &p_mesh) { + ERR_FAIL_COND_MSG(p_mesh == this, "Cannot set a mesh as its own shadow mesh."); shadow_mesh = p_mesh; if (shadow_mesh.is_valid()) { RS::get_singleton()->mesh_set_shadow_mesh(mesh, shadow_mesh->get_rid()); diff --git a/scene/resources/navigation_mesh.h b/scene/resources/navigation_mesh.h index 741cea0791..1b3db5bac2 100644 --- a/scene/resources/navigation_mesh.h +++ b/scene/resources/navigation_mesh.h @@ -33,6 +33,7 @@ #include "core/os/rw_lock.h" #include "scene/resources/mesh.h" +#include "servers/navigation/navigation_globals.h" class NavigationMesh : public Resource { GDCLASS(NavigationMesh, Resource); @@ -77,8 +78,8 @@ public: }; protected: - float cell_size = 0.25f; // Must match ProjectSettings default 3D cell_size and NavigationServer NavMap cell_size. - float cell_height = 0.25f; // Must match ProjectSettings default 3D cell_height and NavigationServer NavMap cell_height. + float cell_size = NavigationDefaults3D::navmesh_cell_size; + float cell_height = NavigationDefaults3D::navmesh_cell_height; float border_size = 0.0f; float agent_height = 1.5f; float agent_radius = 0.5f; diff --git a/scene/resources/packed_scene.cpp b/scene/resources/packed_scene.cpp index 900629f5f8..27db65bb1a 100644 --- a/scene/resources/packed_scene.cpp +++ b/scene/resources/packed_scene.cpp @@ -1880,6 +1880,16 @@ Vector<NodePath> SceneState::get_editable_instances() const { return editable_instances; } +Ref<Resource> SceneState::get_sub_resource(const String &p_path) { + for (const Variant &v : variants) { + const Ref<Resource> &res = v; + if (res.is_valid() && res->get_path() == p_path) { + return res; + } + } + return Ref<Resource>(); +} + //add int SceneState::add_name(const StringName &p_name) { @@ -2199,7 +2209,7 @@ void PackedScene::_bind_methods() { ClassDB::bind_method(D_METHOD("_get_bundled_scene"), &PackedScene::_get_bundled_scene); ClassDB::bind_method(D_METHOD("get_state"), &PackedScene::get_state); - ADD_PROPERTY(PropertyInfo(Variant::DICTIONARY, "_bundled"), "_set_bundled_scene", "_get_bundled_scene"); + ADD_PROPERTY(PropertyInfo(Variant::DICTIONARY, "_bundled", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_INTERNAL), "_set_bundled_scene", "_get_bundled_scene"); BIND_ENUM_CONSTANT(GEN_EDIT_STATE_DISABLED); BIND_ENUM_CONSTANT(GEN_EDIT_STATE_INSTANCE); diff --git a/scene/resources/packed_scene.h b/scene/resources/packed_scene.h index e26b9f7b90..d27def1760 100644 --- a/scene/resources/packed_scene.h +++ b/scene/resources/packed_scene.h @@ -195,6 +195,7 @@ public: bool has_connection(const NodePath &p_node_from, const StringName &p_signal, const NodePath &p_node_to, const StringName &p_method, bool p_no_inheritance = false); Vector<NodePath> get_editable_instances() const; + Ref<Resource> get_sub_resource(const String &p_path); //build API diff --git a/scene/resources/particle_process_material.cpp b/scene/resources/particle_process_material.cpp index ee986f5820..8cfe4c92b7 100644 --- a/scene/resources/particle_process_material.cpp +++ b/scene/resources/particle_process_material.cpp @@ -110,6 +110,7 @@ void ParticleProcessMaterial::init_shaders() { shader_names->emission_ring_height = "emission_ring_height"; shader_names->emission_ring_radius = "emission_ring_radius"; shader_names->emission_ring_inner_radius = "emission_ring_inner_radius"; + shader_names->emission_ring_cone_angle = "emission_ring_cone_angle"; shader_names->emission_shape_offset = "emission_shape_offset"; shader_names->emission_shape_scale = "emission_shape_scale"; @@ -269,6 +270,7 @@ void ParticleProcessMaterial::_update_shader() { code += "uniform float " + shader_names->emission_ring_height + ";\n"; code += "uniform float " + shader_names->emission_ring_radius + ";\n"; code += "uniform float " + shader_names->emission_ring_inner_radius + ";\n"; + code += "uniform float " + shader_names->emission_ring_cone_angle + ";\n"; } break; case EMISSION_SHAPE_MAX: { // Max value for validity check. break; @@ -643,8 +645,14 @@ void ParticleProcessMaterial::_update_shader() { code += " pos = texelFetch(emission_texture_points, emission_tex_ofs, 0).xyz;\n"; } if (emission_shape == EMISSION_SHAPE_RING) { + code += " float radius_clamped = max(0.001, emission_ring_radius);\n"; + code += " float top_radius = max(radius_clamped - tan(radians(90.0 - emission_ring_cone_angle)) * emission_ring_height, 0.0);\n"; + code += " float y_pos = rand_from_seed(alt_seed);\n"; + code += " float skew = max(min(radius_clamped, top_radius) / max(radius_clamped, top_radius), 0.5);\n"; + code += " y_pos = radius_clamped < top_radius ? pow(y_pos, skew) : 1.0 - pow(y_pos, skew);\n"; code += " float ring_spawn_angle = rand_from_seed(alt_seed) * 2.0 * pi;\n"; - code += " float ring_random_radius = sqrt(rand_from_seed(alt_seed) * (emission_ring_radius * emission_ring_radius - emission_ring_inner_radius * emission_ring_inner_radius) + emission_ring_inner_radius * emission_ring_inner_radius);\n"; + code += " float ring_random_radius = sqrt(rand_from_seed(alt_seed) * (radius_clamped * radius_clamped - emission_ring_inner_radius * emission_ring_inner_radius) + emission_ring_inner_radius * emission_ring_inner_radius);\n"; + code += " ring_random_radius = mix(ring_random_radius, ring_random_radius * (top_radius / radius_clamped), y_pos);\n"; code += " vec3 axis = emission_ring_axis == vec3(0.0) ? vec3(0.0, 0.0, 1.0) : normalize(emission_ring_axis);\n"; code += " vec3 ortho_axis = vec3(0.0);\n"; code += " if (abs(axis) == vec3(1.0, 0.0, 0.0)) {\n"; @@ -662,7 +670,7 @@ void ParticleProcessMaterial::_update_shader() { code += " vec3(axis.z * axis.x * oc - axis.y * s, axis.z * axis.y * oc + axis.x * s, c + axis.z * axis.z * oc)\n"; code += " ) * ortho_axis;\n"; code += " ortho_axis = normalize(ortho_axis);\n"; - code += " pos = ortho_axis * ring_random_radius + (rand_from_seed(alt_seed) * emission_ring_height - emission_ring_height / 2.0) * axis;\n"; + code += " pos = ortho_axis * ring_random_radius + (y_pos * emission_ring_height - emission_ring_height / 2.0) * axis;\n"; } code += " }\n"; code += " return pos * emission_shape_scale + emission_shape_offset;\n"; @@ -1615,6 +1623,11 @@ void ParticleProcessMaterial::set_emission_ring_inner_radius(real_t p_radius) { RenderingServer::get_singleton()->material_set_param(_get_material(), shader_names->emission_ring_inner_radius, p_radius); } +void ParticleProcessMaterial::set_emission_ring_cone_angle(real_t p_angle) { + emission_ring_cone_angle = p_angle; + RenderingServer::get_singleton()->material_set_param(_get_material(), shader_names->emission_ring_cone_angle, p_angle); +} + void ParticleProcessMaterial::set_inherit_velocity_ratio(double p_ratio) { inherit_emitter_velocity_ratio = p_ratio; RenderingServer::get_singleton()->material_set_param(_get_material(), shader_names->inherit_emitter_velocity_ratio, p_ratio); @@ -1664,6 +1677,10 @@ real_t ParticleProcessMaterial::get_emission_ring_inner_radius() const { return emission_ring_inner_radius; } +real_t ParticleProcessMaterial::get_emission_ring_cone_angle() const { + return emission_ring_cone_angle; +} + void ParticleProcessMaterial::set_emission_shape_offset(const Vector3 &p_emission_shape_offset) { emission_shape_offset = p_emission_shape_offset; RenderingServer::get_singleton()->material_set_param(_get_material(), shader_names->emission_shape_offset, p_emission_shape_offset); @@ -2015,6 +2032,9 @@ void ParticleProcessMaterial::_bind_methods() { ClassDB::bind_method(D_METHOD("set_emission_ring_inner_radius", "inner_radius"), &ParticleProcessMaterial::set_emission_ring_inner_radius); ClassDB::bind_method(D_METHOD("get_emission_ring_inner_radius"), &ParticleProcessMaterial::get_emission_ring_inner_radius); + ClassDB::bind_method(D_METHOD("set_emission_ring_cone_angle", "cone_angle"), &ParticleProcessMaterial::set_emission_ring_cone_angle); + ClassDB::bind_method(D_METHOD("get_emission_ring_cone_angle"), &ParticleProcessMaterial::get_emission_ring_cone_angle); + ClassDB::bind_method(D_METHOD("set_emission_shape_offset", "emission_shape_offset"), &ParticleProcessMaterial::set_emission_shape_offset); ClassDB::bind_method(D_METHOD("get_emission_shape_offset"), &ParticleProcessMaterial::get_emission_shape_offset); @@ -2096,9 +2116,10 @@ void ParticleProcessMaterial::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "emission_color_texture", PROPERTY_HINT_RESOURCE_TYPE, "Texture2D"), "set_emission_color_texture", "get_emission_color_texture"); ADD_PROPERTY(PropertyInfo(Variant::INT, "emission_point_count", PROPERTY_HINT_RANGE, "0,1000000,1"), "set_emission_point_count", "get_emission_point_count"); ADD_PROPERTY(PropertyInfo(Variant::VECTOR3, "emission_ring_axis"), "set_emission_ring_axis", "get_emission_ring_axis"); - ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "emission_ring_height"), "set_emission_ring_height", "get_emission_ring_height"); - ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "emission_ring_radius"), "set_emission_ring_radius", "get_emission_ring_radius"); - ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "emission_ring_inner_radius"), "set_emission_ring_inner_radius", "get_emission_ring_inner_radius"); + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "emission_ring_height", PROPERTY_HINT_RANGE, "0,1000,0.01,or_greater"), "set_emission_ring_height", "get_emission_ring_height"); + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "emission_ring_radius", PROPERTY_HINT_RANGE, "0,1000,0.01,or_greater"), "set_emission_ring_radius", "get_emission_ring_radius"); + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "emission_ring_inner_radius", PROPERTY_HINT_RANGE, "0,1000,0.01,or_greater"), "set_emission_ring_inner_radius", "get_emission_ring_inner_radius"); + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "emission_ring_cone_angle", PROPERTY_HINT_RANGE, "0,90,0.01,degrees"), "set_emission_ring_cone_angle", "get_emission_ring_cone_angle"); ADD_SUBGROUP("Angle", ""); ADD_MIN_MAX_PROPERTY("angle", "-720,720,0.1,or_less,or_greater,degrees", PARAM_ANGLE); ADD_PROPERTYI(PropertyInfo(Variant::OBJECT, "angle_curve", PROPERTY_HINT_RESOURCE_TYPE, "CurveTexture"), "set_param_texture", "get_param_texture", PARAM_ANGLE); @@ -2276,6 +2297,7 @@ ParticleProcessMaterial::ParticleProcessMaterial() : set_emission_ring_height(1); set_emission_ring_radius(1); set_emission_ring_inner_radius(0); + set_emission_ring_cone_angle(90); set_emission_shape_offset(Vector3(0.0, 0.0, 0.0)); set_emission_shape_scale(Vector3(1.0, 1.0, 1.0)); diff --git a/scene/resources/particle_process_material.h b/scene/resources/particle_process_material.h index 25046b51cd..12e3fbb64e 100644 --- a/scene/resources/particle_process_material.h +++ b/scene/resources/particle_process_material.h @@ -259,6 +259,7 @@ private: StringName emission_ring_height; StringName emission_ring_radius; StringName emission_ring_inner_radius; + StringName emission_ring_cone_angle; StringName emission_shape_offset; StringName emission_shape_scale; @@ -325,6 +326,7 @@ private: real_t emission_ring_height = 0.0f; real_t emission_ring_radius = 0.0f; real_t emission_ring_inner_radius = 0.0f; + real_t emission_ring_cone_angle = 0.0f; int emission_point_count = 1; Vector3 emission_shape_offset; Vector3 emission_shape_scale; @@ -417,6 +419,7 @@ public: void set_emission_ring_height(real_t p_height); void set_emission_ring_radius(real_t p_radius); void set_emission_ring_inner_radius(real_t p_radius); + void set_emission_ring_cone_angle(real_t p_angle); void set_emission_point_count(int p_count); EmissionShape get_emission_shape() const; @@ -429,6 +432,7 @@ public: real_t get_emission_ring_height() const; real_t get_emission_ring_radius() const; real_t get_emission_ring_inner_radius() const; + real_t get_emission_ring_cone_angle() const; int get_emission_point_count() const; void set_turbulence_enabled(bool p_turbulence_enabled); diff --git a/scene/resources/portable_compressed_texture.cpp b/scene/resources/portable_compressed_texture.cpp index 002db30379..06b5ec6d5a 100644 --- a/scene/resources/portable_compressed_texture.cpp +++ b/scene/resources/portable_compressed_texture.cpp @@ -345,7 +345,7 @@ void PortableCompressedTexture2D::_bind_methods() { ClassDB::bind_static_method("PortableCompressedTexture2D", D_METHOD("set_keep_all_compressed_buffers", "keep"), &PortableCompressedTexture2D::set_keep_all_compressed_buffers); ClassDB::bind_static_method("PortableCompressedTexture2D", D_METHOD("is_keeping_all_compressed_buffers"), &PortableCompressedTexture2D::is_keeping_all_compressed_buffers); - ADD_PROPERTY(PropertyInfo(Variant::PACKED_BYTE_ARRAY, "_data", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR), "_set_data", "_get_data"); + ADD_PROPERTY(PropertyInfo(Variant::PACKED_BYTE_ARRAY, "_data", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR | PROPERTY_USAGE_INTERNAL), "_set_data", "_get_data"); ADD_PROPERTY(PropertyInfo(Variant::VECTOR2, "size_override", PROPERTY_HINT_NONE, "suffix:px"), "set_size_override", "get_size_override"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "keep_compressed_buffer"), "set_keep_compressed_buffer", "is_keeping_compressed_buffer"); diff --git a/scene/resources/shader.compat.inc b/scene/resources/shader.compat.inc new file mode 100644 index 0000000000..b68020605f --- /dev/null +++ b/scene/resources/shader.compat.inc @@ -0,0 +1,46 @@ +/**************************************************************************/ +/* shader.compat.inc */ +/**************************************************************************/ +/* 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. */ +/**************************************************************************/ + +#ifndef DISABLE_DEPRECATED + +void Shader::_set_default_texture_parameter_bind_compat_95126(const StringName &p_name, const Ref<Texture2D> &p_texture, int p_index) { + set_default_texture_parameter(p_name, p_texture, p_index); +} + +Ref<Texture2D> Shader::_get_default_texture_parameter_bind_compat_95126(const StringName &p_name, int p_index) const { + return get_default_texture_parameter(p_name, p_index); +} + +void Shader::_bind_compatibility_methods() { + ClassDB::bind_compatibility_method(D_METHOD("set_default_texture_parameter", "name", "texture", "index"), &Shader::_set_default_texture_parameter_bind_compat_95126, DEFVAL(0)); + ClassDB::bind_compatibility_method(D_METHOD("get_default_texture_parameter", "name", "index"), &Shader::_get_default_texture_parameter_bind_compat_95126, DEFVAL(0)); +} + +#endif // DISABLE_DEPRECATED diff --git a/scene/resources/shader.cpp b/scene/resources/shader.cpp index dfe5bd4a47..f343229cd8 100644 --- a/scene/resources/shader.cpp +++ b/scene/resources/shader.cpp @@ -29,6 +29,7 @@ /**************************************************************************/ #include "shader.h" +#include "shader.compat.inc" #include "core/io/file_access.h" #include "servers/rendering/shader_language.h" @@ -185,10 +186,10 @@ RID Shader::get_rid() const { return shader; } -void Shader::set_default_texture_parameter(const StringName &p_name, const Ref<Texture2D> &p_texture, int p_index) { +void Shader::set_default_texture_parameter(const StringName &p_name, const Ref<Texture> &p_texture, int p_index) { if (p_texture.is_valid()) { if (!default_textures.has(p_name)) { - default_textures[p_name] = HashMap<int, Ref<Texture2D>>(); + default_textures[p_name] = HashMap<int, Ref<Texture>>(); } default_textures[p_name][p_index] = p_texture; RS::get_singleton()->shader_set_default_texture_parameter(shader, p_name, p_texture->get_rid(), p_index); @@ -206,7 +207,7 @@ void Shader::set_default_texture_parameter(const StringName &p_name, const Ref<T emit_changed(); } -Ref<Texture2D> Shader::get_default_texture_parameter(const StringName &p_name, int p_index) const { +Ref<Texture> Shader::get_default_texture_parameter(const StringName &p_name, int p_index) const { if (default_textures.has(p_name) && default_textures[p_name].has(p_index)) { return default_textures[p_name][p_index]; } @@ -214,7 +215,7 @@ Ref<Texture2D> Shader::get_default_texture_parameter(const StringName &p_name, i } void Shader::get_default_texture_parameter_list(List<StringName> *r_textures) const { - for (const KeyValue<StringName, HashMap<int, Ref<Texture2D>>> &E : default_textures) { + for (const KeyValue<StringName, HashMap<int, Ref<Texture>>> &E : default_textures) { r_textures->push_back(E.key); } } diff --git a/scene/resources/shader.h b/scene/resources/shader.h index 921143c219..682fbd7ea6 100644 --- a/scene/resources/shader.h +++ b/scene/resources/shader.h @@ -58,7 +58,7 @@ private: String code; String include_path; - HashMap<StringName, HashMap<int, Ref<Texture2D>>> default_textures; + HashMap<StringName, HashMap<int, Ref<Texture>>> default_textures; void _dependency_changed(); void _recompile(); @@ -66,6 +66,12 @@ private: Array _get_shader_uniform_list(bool p_get_groups = false); protected: +#ifndef DISABLE_DEPRECATED + void _set_default_texture_parameter_bind_compat_95126(const StringName &p_name, const Ref<Texture2D> &p_texture, int p_index = 0); + Ref<Texture2D> _get_default_texture_parameter_bind_compat_95126(const StringName &p_name, int p_index = 0) const; + static void _bind_compatibility_methods(); +#endif // DISABLE_DEPRECATED + static void _bind_methods(); public: @@ -80,8 +86,8 @@ public: void get_shader_uniform_list(List<PropertyInfo> *p_params, bool p_get_groups = false) const; - void set_default_texture_parameter(const StringName &p_name, const Ref<Texture2D> &p_texture, int p_index = 0); - Ref<Texture2D> get_default_texture_parameter(const StringName &p_name, int p_index = 0) const; + void set_default_texture_parameter(const StringName &p_name, const Ref<Texture> &p_texture, int p_index = 0); + Ref<Texture> get_default_texture_parameter(const StringName &p_name, int p_index = 0) const; void get_default_texture_parameter_list(List<StringName> *r_textures) const; virtual bool is_text_shader() const; diff --git a/scene/resources/skeleton_profile.cpp b/scene/resources/skeleton_profile.cpp index 2c1d3d4a4c..c2d77ec7ff 100644 --- a/scene/resources/skeleton_profile.cpp +++ b/scene/resources/skeleton_profile.cpp @@ -132,7 +132,10 @@ void SkeletonProfile::_validate_property(PropertyInfo &p_property) const { if (p_property.name == ("root_bone") || p_property.name == ("scale_base_bone")) { String hint = ""; for (int i = 0; i < bones.size(); i++) { - hint += i == 0 ? String(bones[i].bone_name) : "," + String(bones[i].bone_name); + if (i > 0) { + hint += ","; + } + hint += String(bones[i].bone_name); } p_property.hint_string = hint; } diff --git a/scene/resources/sprite_frames.cpp b/scene/resources/sprite_frames.cpp index 6e43ea9b17..dac0ceaa78 100644 --- a/scene/resources/sprite_frames.cpp +++ b/scene/resources/sprite_frames.cpp @@ -106,6 +106,12 @@ bool SpriteFrames::has_animation(const StringName &p_anim) const { return animations.has(p_anim); } +void SpriteFrames::duplicate_animation(const StringName &p_from, const StringName &p_to) { + ERR_FAIL_COND_MSG(!animations.has(p_from), vformat("SpriteFrames doesn't have animation '%s'.", p_from)); + ERR_FAIL_COND_MSG(animations.has(p_to), vformat("Animation '%s' already exists.", p_to)); + animations[p_to] = animations[p_from]; +} + void SpriteFrames::remove_animation(const StringName &p_anim) { animations.erase(p_anim); } @@ -246,6 +252,7 @@ void SpriteFrames::get_argument_options(const StringName &p_function, int p_idx, void SpriteFrames::_bind_methods() { ClassDB::bind_method(D_METHOD("add_animation", "anim"), &SpriteFrames::add_animation); ClassDB::bind_method(D_METHOD("has_animation", "anim"), &SpriteFrames::has_animation); + ClassDB::bind_method(D_METHOD("duplicate_animation", "anim_from", "anim_to"), &SpriteFrames::duplicate_animation); ClassDB::bind_method(D_METHOD("remove_animation", "anim"), &SpriteFrames::remove_animation); ClassDB::bind_method(D_METHOD("rename_animation", "anim", "newname"), &SpriteFrames::rename_animation); diff --git a/scene/resources/sprite_frames.h b/scene/resources/sprite_frames.h index 0e0bb28d71..8d5b4232cf 100644 --- a/scene/resources/sprite_frames.h +++ b/scene/resources/sprite_frames.h @@ -60,6 +60,7 @@ protected: public: void add_animation(const StringName &p_anim); bool has_animation(const StringName &p_anim) const; + void duplicate_animation(const StringName &p_from, const StringName &p_to); void remove_animation(const StringName &p_anim); void rename_animation(const StringName &p_prev, const StringName &p_next); diff --git a/scene/resources/visual_shader.cpp b/scene/resources/visual_shader.cpp index 1fa52b9c73..d0e55f4065 100644 --- a/scene/resources/visual_shader.cpp +++ b/scene/resources/visual_shader.cpp @@ -32,6 +32,7 @@ #include "core/templates/rb_map.h" #include "core/templates/vmap.h" +#include "core/variant/variant_utility.h" #include "servers/rendering/shader_types.h" #include "visual_shader_nodes.h" #include "visual_shader_particle_nodes.h" @@ -832,7 +833,7 @@ VisualShader::Type VisualShader::get_shader_type() const { } void VisualShader::add_varying(const String &p_name, VaryingMode p_mode, VaryingType p_type) { - ERR_FAIL_COND(!p_name.is_valid_identifier()); + ERR_FAIL_COND(!p_name.is_valid_ascii_identifier()); ERR_FAIL_INDEX((int)p_mode, (int)VARYING_MODE_MAX); ERR_FAIL_INDEX((int)p_type, (int)VARYING_TYPE_MAX); ERR_FAIL_COND(varyings.has(p_name)); @@ -897,6 +898,44 @@ VisualShader::VaryingType VisualShader::get_varying_type(const String &p_name) { return varyings[p_name].type; } +void VisualShader::_set_preview_shader_parameter(const String &p_name, const Variant &p_value) { +#ifdef TOOLS_ENABLED + if (Engine::get_singleton()->is_editor_hint()) { + if (p_value.get_type() == Variant::NIL) { + if (!preview_params.erase(p_name)) { + return; + } + } else { + Variant *var = preview_params.getptr(p_name); + if (var != nullptr && *var == p_value) { + return; + } + preview_params.insert(p_name, p_value); + } + emit_changed(); + } +#endif // TOOLS_ENABLED +} + +Variant VisualShader::_get_preview_shader_parameter(const String &p_name) const { +#ifdef TOOLS_ENABLED + if (Engine::get_singleton()->is_editor_hint()) { + ERR_FAIL_COND_V(!preview_params.has(p_name), Variant()); + return preview_params.get(p_name); + } +#endif // TOOLS_ENABLED + return Variant(); +} + +bool VisualShader::_has_preview_shader_parameter(const String &p_name) const { +#ifdef TOOLS_ENABLED + if (Engine::get_singleton()->is_editor_hint()) { + return preview_params.has(p_name); + } +#endif // TOOLS_ENABLED + return false; +} + void VisualShader::add_node(Type p_type, const Ref<VisualShaderNode> &p_node, const Vector2 &p_position, int p_id) { ERR_FAIL_COND(p_node.is_null()); ERR_FAIL_COND(p_id < 2); @@ -1695,7 +1734,16 @@ bool VisualShader::_set(const StringName &p_name, const Variant &p_value) { } _queue_update(); return true; - } else if (prop_name.begins_with("nodes/")) { + } +#ifdef TOOLS_ENABLED + else if (prop_name.begins_with("preview_params/") && Engine::get_singleton()->is_editor_hint()) { + String param_name = prop_name.get_slicec('/', 1); + Variant value = VariantUtilityFunctions::str_to_var(p_value); + preview_params[param_name] = value; + return true; + } +#endif + else if (prop_name.begins_with("nodes/")) { String typestr = prop_name.get_slicec('/', 1); Type type = TYPE_VERTEX; for (int i = 0; i < TYPE_MAX; i++) { @@ -1767,7 +1815,19 @@ bool VisualShader::_get(const StringName &p_name, Variant &r_ret) const { r_ret = String(); } return true; - } else if (prop_name.begins_with("nodes/")) { + } +#ifdef TOOLS_ENABLED + else if (prop_name.begins_with("preview_params/") && Engine::get_singleton()->is_editor_hint()) { + String param_name = prop_name.get_slicec('/', 1); + if (preview_params.has(param_name)) { + r_ret = VariantUtilityFunctions::var_to_str(preview_params[param_name]); + } else { + r_ret = String(); + } + return true; + } +#endif // TOOLS_ENABLED + else if (prop_name.begins_with("nodes/")) { String typestr = prop_name.get_slicec('/', 1); Type type = TYPE_VERTEX; for (int i = 0; i < TYPE_MAX; i++) { @@ -1864,6 +1924,14 @@ void VisualShader::_get_property_list(List<PropertyInfo> *p_list) const { p_list->push_back(PropertyInfo(Variant::STRING, vformat("%s/%s", PNAME("varyings"), E.key), PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR)); } +#ifdef TOOLS_ENABLED + if (Engine::get_singleton()->is_editor_hint()) { + for (const KeyValue<String, Variant> &E : preview_params) { + p_list->push_back(PropertyInfo(Variant::STRING, vformat("%s/%s", PNAME("preview_params"), E.key), PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR)); + } + } +#endif // TOOLS_ENABLED + for (int i = 0; i < TYPE_MAX; i++) { for (const KeyValue<int, Node> &E : graph[i].nodes) { String prop_name = "nodes/"; @@ -2885,7 +2953,7 @@ void VisualShader::_update_shader() const { const_cast<VisualShader *>(this)->set_code(final_code); for (int i = 0; i < default_tex_params.size(); i++) { int j = 0; - for (List<Ref<Texture2D>>::ConstIterator itr = default_tex_params[i].params.begin(); itr != default_tex_params[i].params.end(); ++itr, ++j) { + for (List<Ref<Texture>>::ConstIterator itr = default_tex_params[i].params.begin(); itr != default_tex_params[i].params.end(); ++itr, ++j) { const_cast<VisualShader *>(this)->set_default_texture_parameter(default_tex_params[i].name, *itr, j); } } @@ -2943,6 +3011,10 @@ void VisualShader::_bind_methods() { ClassDB::bind_method(D_METHOD("remove_varying", "name"), &VisualShader::remove_varying); ClassDB::bind_method(D_METHOD("has_varying", "name"), &VisualShader::has_varying); + ClassDB::bind_method(D_METHOD("_set_preview_shader_parameter", "name", "value"), &VisualShader::_set_preview_shader_parameter); + ClassDB::bind_method(D_METHOD("_get_preview_shader_parameter", "name"), &VisualShader::_get_preview_shader_parameter); + ClassDB::bind_method(D_METHOD("_has_preview_shader_parameter", "name"), &VisualShader::_has_preview_shader_parameter); + ClassDB::bind_method(D_METHOD("_update_shader"), &VisualShader::_update_shader); ADD_PROPERTY(PropertyInfo(Variant::VECTOR2, "graph_offset", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR), "set_graph_offset", "get_graph_offset"); @@ -3005,243 +3077,247 @@ VisualShader::VisualShader() { const VisualShaderNodeInput::Port VisualShaderNodeInput::ports[] = { // Spatial - // Spatial, Vertex - { Shader::MODE_SPATIAL, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_VECTOR_3D, "vertex", "VERTEX" }, - { Shader::MODE_SPATIAL, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_SCALAR_INT, "vertex_id", "VERTEX_ID" }, - { Shader::MODE_SPATIAL, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_VECTOR_3D, "normal", "NORMAL" }, - { Shader::MODE_SPATIAL, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_VECTOR_3D, "tangent", "TANGENT" }, + // Node3D, Vertex { Shader::MODE_SPATIAL, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_VECTOR_3D, "binormal", "BINORMAL" }, - { Shader::MODE_SPATIAL, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_VECTOR_2D, "uv", "UV" }, - { Shader::MODE_SPATIAL, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_VECTOR_2D, "uv2", "UV2" }, + { Shader::MODE_SPATIAL, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_VECTOR_3D, "camera_direction_world", "CAMERA_DIRECTION_WORLD" }, + { Shader::MODE_SPATIAL, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_VECTOR_3D, "camera_position_world", "CAMERA_POSITION_WORLD" }, + { Shader::MODE_SPATIAL, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_SCALAR_UINT, "camera_visible_layers", "CAMERA_VISIBLE_LAYERS" }, + { Shader::MODE_SPATIAL, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_SCALAR, "clip_space_far", "CLIP_SPACE_FAR" }, { Shader::MODE_SPATIAL, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_VECTOR_4D, "color", "COLOR" }, - { Shader::MODE_SPATIAL, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_SCALAR, "point_size", "POINT_SIZE" }, - { Shader::MODE_SPATIAL, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_SCALAR_INT, "instance_id", "INSTANCE_ID" }, + { Shader::MODE_SPATIAL, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_VECTOR_4D, "custom0", "CUSTOM0" }, + { Shader::MODE_SPATIAL, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_VECTOR_4D, "custom1", "CUSTOM1" }, + { Shader::MODE_SPATIAL, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_VECTOR_4D, "custom2", "CUSTOM2" }, + { Shader::MODE_SPATIAL, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_VECTOR_4D, "custom3", "CUSTOM3" }, + { Shader::MODE_SPATIAL, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_SCALAR, "exposure", "EXPOSURE" }, + { Shader::MODE_SPATIAL, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_VECTOR_3D, "eye_offset", "EYE_OFFSET" }, { Shader::MODE_SPATIAL, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_VECTOR_4D, "instance_custom", "INSTANCE_CUSTOM" }, - { Shader::MODE_SPATIAL, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_SCALAR, "roughness", "ROUGHNESS" }, + { Shader::MODE_SPATIAL, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_SCALAR_INT, "instance_id", "INSTANCE_ID" }, + { Shader::MODE_SPATIAL, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_TRANSFORM, "inv_projection_matrix", "INV_PROJECTION_MATRIX" }, + { Shader::MODE_SPATIAL, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_TRANSFORM, "inv_view_matrix", "INV_VIEW_MATRIX" }, { Shader::MODE_SPATIAL, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_TRANSFORM, "model_matrix", "MODEL_MATRIX" }, { Shader::MODE_SPATIAL, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_TRANSFORM, "modelview_matrix", "MODELVIEW_MATRIX" }, - { Shader::MODE_SPATIAL, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_TRANSFORM, "inv_view_matrix", "INV_VIEW_MATRIX" }, - { Shader::MODE_SPATIAL, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_TRANSFORM, "view_matrix", "VIEW_MATRIX" }, + { Shader::MODE_SPATIAL, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_VECTOR_3D, "node_position_view", "NODE_POSITION_VIEW" }, + { Shader::MODE_SPATIAL, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_VECTOR_3D, "node_position_world", "NODE_POSITION_WORLD" }, + { Shader::MODE_SPATIAL, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_VECTOR_3D, "normal", "NORMAL" }, + { Shader::MODE_SPATIAL, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_BOOLEAN, "output_is_srgb", "OUTPUT_IS_SRGB" }, + { Shader::MODE_SPATIAL, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_SCALAR, "point_size", "POINT_SIZE" }, { Shader::MODE_SPATIAL, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_TRANSFORM, "projection_matrix", "PROJECTION_MATRIX" }, - { Shader::MODE_SPATIAL, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_TRANSFORM, "inv_projection_matrix", "INV_PROJECTION_MATRIX" }, + { Shader::MODE_SPATIAL, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_SCALAR, "roughness", "ROUGHNESS" }, + { Shader::MODE_SPATIAL, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_VECTOR_3D, "tangent", "TANGENT" }, { Shader::MODE_SPATIAL, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_SCALAR, "time", "TIME" }, - { Shader::MODE_SPATIAL, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_SCALAR, "exposure", "EXPOSURE" }, - { Shader::MODE_SPATIAL, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_VECTOR_2D, "viewport_size", "VIEWPORT_SIZE" }, - { Shader::MODE_SPATIAL, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_BOOLEAN, "output_is_srgb", "OUTPUT_IS_SRGB" }, + { Shader::MODE_SPATIAL, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_VECTOR_2D, "uv", "UV" }, + { Shader::MODE_SPATIAL, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_VECTOR_2D, "uv2", "UV2" }, + { Shader::MODE_SPATIAL, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_VECTOR_3D, "vertex", "VERTEX" }, + { Shader::MODE_SPATIAL, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_SCALAR_INT, "vertex_id", "VERTEX_ID" }, { Shader::MODE_SPATIAL, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_SCALAR_INT, "view_index", "VIEW_INDEX" }, + { Shader::MODE_SPATIAL, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_TRANSFORM, "view_matrix", "VIEW_MATRIX" }, { Shader::MODE_SPATIAL, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_SCALAR_INT, "view_mono_left", "VIEW_MONO_LEFT" }, { Shader::MODE_SPATIAL, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_SCALAR_INT, "view_right", "VIEW_RIGHT" }, - { Shader::MODE_SPATIAL, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_VECTOR_3D, "eye_offset", "EYE_OFFSET" }, - { Shader::MODE_SPATIAL, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_VECTOR_3D, "node_position_world", "NODE_POSITION_WORLD" }, - { Shader::MODE_SPATIAL, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_VECTOR_3D, "camera_position_world", "CAMERA_POSITION_WORLD" }, - { Shader::MODE_SPATIAL, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_VECTOR_3D, "camera_direction_world", "CAMERA_DIRECTION_WORLD" }, - { Shader::MODE_SPATIAL, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_SCALAR_UINT, "camera_visible_layers", "CAMERA_VISIBLE_LAYERS" }, - { Shader::MODE_SPATIAL, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_VECTOR_3D, "node_position_view", "NODE_POSITION_VIEW" }, - { Shader::MODE_SPATIAL, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_VECTOR_4D, "custom0", "CUSTOM0" }, - { Shader::MODE_SPATIAL, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_VECTOR_4D, "custom1", "CUSTOM1" }, - { Shader::MODE_SPATIAL, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_VECTOR_4D, "custom2", "CUSTOM2" }, - { Shader::MODE_SPATIAL, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_VECTOR_4D, "custom3", "CUSTOM3" }, + { Shader::MODE_SPATIAL, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_VECTOR_2D, "viewport_size", "VIEWPORT_SIZE" }, - // Spatial, Fragment - { Shader::MODE_SPATIAL, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_VECTOR_4D, "fragcoord", "FRAGCOORD" }, - { Shader::MODE_SPATIAL, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_VECTOR_3D, "vertex", "VERTEX" }, - { Shader::MODE_SPATIAL, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_VECTOR_3D, "normal", "NORMAL" }, - { Shader::MODE_SPATIAL, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_VECTOR_3D, "tangent", "TANGENT" }, + // Node3D, Fragment { Shader::MODE_SPATIAL, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_VECTOR_3D, "binormal", "BINORMAL" }, - { Shader::MODE_SPATIAL, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_VECTOR_3D, "view", "VIEW" }, - { Shader::MODE_SPATIAL, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_VECTOR_2D, "uv", "UV" }, - { Shader::MODE_SPATIAL, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_VECTOR_2D, "uv2", "UV2" }, + { Shader::MODE_SPATIAL, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_VECTOR_3D, "camera_direction_world", "CAMERA_DIRECTION_WORLD" }, + { Shader::MODE_SPATIAL, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_VECTOR_3D, "camera_position_world", "CAMERA_POSITION_WORLD" }, + { Shader::MODE_SPATIAL, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_SCALAR_UINT, "camera_visible_layers", "CAMERA_VISIBLE_LAYERS" }, + { Shader::MODE_SPATIAL, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_SCALAR, "clip_space_far", "CLIP_SPACE_FAR" }, { Shader::MODE_SPATIAL, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_VECTOR_4D, "color", "COLOR" }, - { Shader::MODE_SPATIAL, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_VECTOR_2D, "point_coord", "POINT_COORD" }, - { Shader::MODE_SPATIAL, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_VECTOR_2D, "screen_uv", "SCREEN_UV" }, - { Shader::MODE_SPATIAL, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_TRANSFORM, "model_matrix", "MODEL_MATRIX" }, - { Shader::MODE_SPATIAL, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_TRANSFORM, "view_matrix", "VIEW_MATRIX" }, + { Shader::MODE_SPATIAL, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_SCALAR, "exposure", "EXPOSURE" }, + { Shader::MODE_SPATIAL, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_VECTOR_3D, "eye_offset", "EYE_OFFSET" }, + { Shader::MODE_SPATIAL, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_VECTOR_4D, "fragcoord", "FRAGCOORD" }, + { Shader::MODE_SPATIAL, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_BOOLEAN, "front_facing", "FRONT_FACING" }, + { Shader::MODE_SPATIAL, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_TRANSFORM, "inv_projection_matrix", "INV_PROJECTION_MATRIX" }, { Shader::MODE_SPATIAL, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_TRANSFORM, "inv_view_matrix", "INV_VIEW_MATRIX" }, + { Shader::MODE_SPATIAL, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_TRANSFORM, "model_matrix", "MODEL_MATRIX" }, + { Shader::MODE_SPATIAL, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_VECTOR_3D, "node_position_view", "NODE_POSITION_VIEW" }, + { Shader::MODE_SPATIAL, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_VECTOR_3D, "node_position_world", "NODE_POSITION_WORLD" }, + { Shader::MODE_SPATIAL, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_VECTOR_3D, "normal", "NORMAL" }, + { Shader::MODE_SPATIAL, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_BOOLEAN, "output_is_srgb", "OUTPUT_IS_SRGB" }, + { Shader::MODE_SPATIAL, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_VECTOR_2D, "point_coord", "POINT_COORD" }, { Shader::MODE_SPATIAL, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_TRANSFORM, "projection_matrix", "PROJECTION_MATRIX" }, - { Shader::MODE_SPATIAL, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_TRANSFORM, "inv_projection_matrix", "INV_PROJECTION_MATRIX" }, + { Shader::MODE_SPATIAL, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_VECTOR_2D, "screen_uv", "SCREEN_UV" }, + { Shader::MODE_SPATIAL, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_VECTOR_3D, "tangent", "TANGENT" }, { Shader::MODE_SPATIAL, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_SCALAR, "time", "TIME" }, - { Shader::MODE_SPATIAL, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_SCALAR, "exposure", "EXPOSURE" }, - { Shader::MODE_SPATIAL, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_VECTOR_2D, "viewport_size", "VIEWPORT_SIZE" }, - { Shader::MODE_SPATIAL, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_BOOLEAN, "output_is_srgb", "OUTPUT_IS_SRGB" }, - { Shader::MODE_SPATIAL, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_BOOLEAN, "front_facing", "FRONT_FACING" }, + { Shader::MODE_SPATIAL, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_VECTOR_2D, "uv", "UV" }, + { Shader::MODE_SPATIAL, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_VECTOR_2D, "uv2", "UV2" }, + { Shader::MODE_SPATIAL, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_VECTOR_3D, "vertex", "VERTEX" }, + { Shader::MODE_SPATIAL, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_VECTOR_3D, "view", "VIEW" }, { Shader::MODE_SPATIAL, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_SCALAR_INT, "view_index", "VIEW_INDEX" }, + { Shader::MODE_SPATIAL, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_TRANSFORM, "view_matrix", "VIEW_MATRIX" }, { Shader::MODE_SPATIAL, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_SCALAR_INT, "view_mono_left", "VIEW_MONO_LEFT" }, { Shader::MODE_SPATIAL, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_SCALAR_INT, "view_right", "VIEW_RIGHT" }, - { Shader::MODE_SPATIAL, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_VECTOR_3D, "eye_offset", "EYE_OFFSET" }, - { Shader::MODE_SPATIAL, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_VECTOR_3D, "node_position_world", "NODE_POSITION_WORLD" }, - { Shader::MODE_SPATIAL, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_VECTOR_3D, "camera_position_world", "CAMERA_POSITION_WORLD" }, - { Shader::MODE_SPATIAL, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_VECTOR_3D, "camera_direction_world", "CAMERA_DIRECTION_WORLD" }, - { Shader::MODE_SPATIAL, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_SCALAR_UINT, "camera_visible_layers", "CAMERA_VISIBLE_LAYERS" }, - { Shader::MODE_SPATIAL, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_VECTOR_3D, "node_position_view", "NODE_POSITION_VIEW" }, + { Shader::MODE_SPATIAL, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_VECTOR_2D, "viewport_size", "VIEWPORT_SIZE" }, - // Spatial, Light + // Node3D, Light + { Shader::MODE_SPATIAL, VisualShader::TYPE_LIGHT, VisualShaderNode::PORT_TYPE_VECTOR_3D, "albedo", "ALBEDO" }, + { Shader::MODE_SPATIAL, VisualShader::TYPE_LIGHT, VisualShaderNode::PORT_TYPE_SCALAR, "attenuation", "ATTENUATION" }, + { Shader::MODE_SPATIAL, VisualShader::TYPE_LIGHT, VisualShaderNode::PORT_TYPE_VECTOR_3D, "backlight", "BACKLIGHT" }, + { Shader::MODE_SPATIAL, VisualShader::TYPE_LIGHT, VisualShaderNode::PORT_TYPE_SCALAR, "clip_space_far", "CLIP_SPACE_FAR" }, + { Shader::MODE_SPATIAL, VisualShader::TYPE_LIGHT, VisualShaderNode::PORT_TYPE_VECTOR_3D, "diffuse", "DIFFUSE_LIGHT" }, + { Shader::MODE_SPATIAL, VisualShader::TYPE_LIGHT, VisualShaderNode::PORT_TYPE_SCALAR, "exposure", "EXPOSURE" }, { Shader::MODE_SPATIAL, VisualShader::TYPE_LIGHT, VisualShaderNode::PORT_TYPE_VECTOR_4D, "fragcoord", "FRAGCOORD" }, - { Shader::MODE_SPATIAL, VisualShader::TYPE_LIGHT, VisualShaderNode::PORT_TYPE_VECTOR_3D, "normal", "NORMAL" }, - { Shader::MODE_SPATIAL, VisualShader::TYPE_LIGHT, VisualShaderNode::PORT_TYPE_VECTOR_2D, "uv", "UV" }, - { Shader::MODE_SPATIAL, VisualShader::TYPE_LIGHT, VisualShaderNode::PORT_TYPE_VECTOR_2D, "uv2", "UV2" }, - { Shader::MODE_SPATIAL, VisualShader::TYPE_LIGHT, VisualShaderNode::PORT_TYPE_VECTOR_3D, "view", "VIEW" }, + { Shader::MODE_SPATIAL, VisualShader::TYPE_LIGHT, VisualShaderNode::PORT_TYPE_TRANSFORM, "inv_projection_matrix", "INV_PROJECTION_MATRIX" }, + { Shader::MODE_SPATIAL, VisualShader::TYPE_LIGHT, VisualShaderNode::PORT_TYPE_TRANSFORM, "inv_view_matrix", "INV_VIEW_MATRIX" }, { Shader::MODE_SPATIAL, VisualShader::TYPE_LIGHT, VisualShaderNode::PORT_TYPE_VECTOR_3D, "light", "LIGHT" }, { Shader::MODE_SPATIAL, VisualShader::TYPE_LIGHT, VisualShaderNode::PORT_TYPE_VECTOR_3D, "light_color", "LIGHT_COLOR" }, { Shader::MODE_SPATIAL, VisualShader::TYPE_LIGHT, VisualShaderNode::PORT_TYPE_BOOLEAN, "light_is_directional", "LIGHT_IS_DIRECTIONAL" }, - { Shader::MODE_SPATIAL, VisualShader::TYPE_LIGHT, VisualShaderNode::PORT_TYPE_SCALAR, "attenuation", "ATTENUATION" }, - { Shader::MODE_SPATIAL, VisualShader::TYPE_LIGHT, VisualShaderNode::PORT_TYPE_VECTOR_3D, "albedo", "ALBEDO" }, - { Shader::MODE_SPATIAL, VisualShader::TYPE_LIGHT, VisualShaderNode::PORT_TYPE_VECTOR_3D, "backlight", "BACKLIGHT" }, - { Shader::MODE_SPATIAL, VisualShader::TYPE_LIGHT, VisualShaderNode::PORT_TYPE_VECTOR_3D, "diffuse", "DIFFUSE_LIGHT" }, - { Shader::MODE_SPATIAL, VisualShader::TYPE_LIGHT, VisualShaderNode::PORT_TYPE_VECTOR_3D, "specular", "SPECULAR_LIGHT" }, - { Shader::MODE_SPATIAL, VisualShader::TYPE_LIGHT, VisualShaderNode::PORT_TYPE_SCALAR, "roughness", "ROUGHNESS" }, { Shader::MODE_SPATIAL, VisualShader::TYPE_LIGHT, VisualShaderNode::PORT_TYPE_SCALAR, "metallic", "METALLIC" }, { Shader::MODE_SPATIAL, VisualShader::TYPE_LIGHT, VisualShaderNode::PORT_TYPE_TRANSFORM, "model_matrix", "MODEL_MATRIX" }, - { Shader::MODE_SPATIAL, VisualShader::TYPE_LIGHT, VisualShaderNode::PORT_TYPE_TRANSFORM, "view_matrix", "VIEW_MATRIX" }, - { Shader::MODE_SPATIAL, VisualShader::TYPE_LIGHT, VisualShaderNode::PORT_TYPE_TRANSFORM, "inv_view_matrix", "INV_VIEW_MATRIX" }, + { Shader::MODE_SPATIAL, VisualShader::TYPE_LIGHT, VisualShaderNode::PORT_TYPE_VECTOR_3D, "normal", "NORMAL" }, + { Shader::MODE_SPATIAL, VisualShader::TYPE_LIGHT, VisualShaderNode::PORT_TYPE_BOOLEAN, "output_is_srgb", "OUTPUT_IS_SRGB" }, { Shader::MODE_SPATIAL, VisualShader::TYPE_LIGHT, VisualShaderNode::PORT_TYPE_TRANSFORM, "projection_matrix", "PROJECTION_MATRIX" }, - { Shader::MODE_SPATIAL, VisualShader::TYPE_LIGHT, VisualShaderNode::PORT_TYPE_TRANSFORM, "inv_projection_matrix", "INV_PROJECTION_MATRIX" }, + { Shader::MODE_SPATIAL, VisualShader::TYPE_LIGHT, VisualShaderNode::PORT_TYPE_SCALAR, "roughness", "ROUGHNESS" }, + { Shader::MODE_SPATIAL, VisualShader::TYPE_LIGHT, VisualShaderNode::PORT_TYPE_VECTOR_3D, "specular", "SPECULAR_LIGHT" }, { Shader::MODE_SPATIAL, VisualShader::TYPE_LIGHT, VisualShaderNode::PORT_TYPE_SCALAR, "time", "TIME" }, - { Shader::MODE_SPATIAL, VisualShader::TYPE_LIGHT, VisualShaderNode::PORT_TYPE_SCALAR, "exposure", "EXPOSURE" }, + { Shader::MODE_SPATIAL, VisualShader::TYPE_LIGHT, VisualShaderNode::PORT_TYPE_VECTOR_2D, "uv", "UV" }, + { Shader::MODE_SPATIAL, VisualShader::TYPE_LIGHT, VisualShaderNode::PORT_TYPE_VECTOR_2D, "uv2", "UV2" }, + { Shader::MODE_SPATIAL, VisualShader::TYPE_LIGHT, VisualShaderNode::PORT_TYPE_VECTOR_3D, "view", "VIEW" }, + { Shader::MODE_SPATIAL, VisualShader::TYPE_LIGHT, VisualShaderNode::PORT_TYPE_TRANSFORM, "view_matrix", "VIEW_MATRIX" }, { Shader::MODE_SPATIAL, VisualShader::TYPE_LIGHT, VisualShaderNode::PORT_TYPE_VECTOR_2D, "viewport_size", "VIEWPORT_SIZE" }, - { Shader::MODE_SPATIAL, VisualShader::TYPE_LIGHT, VisualShaderNode::PORT_TYPE_BOOLEAN, "output_is_srgb", "OUTPUT_IS_SRGB" }, // Canvas Item // Canvas Item, Vertex - { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_VECTOR_2D, "vertex", "VERTEX" }, - { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_VECTOR_2D, "uv", "UV" }, - { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_VECTOR_4D, "color", "COLOR" }, - { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_SCALAR, "point_size", "POINT_SIZE" }, - { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_VECTOR_2D, "texture_pixel_size", "TEXTURE_PIXEL_SIZE" }, - { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_TRANSFORM, "model_matrix", "MODEL_MATRIX" }, - { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_TRANSFORM, "canvas_matrix", "CANVAS_MATRIX" }, - { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_TRANSFORM, "screen_matrix", "SCREEN_MATRIX" }, - { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_SCALAR, "time", "TIME" }, { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_BOOLEAN, "at_light_pass", "AT_LIGHT_PASS" }, + { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_TRANSFORM, "canvas_matrix", "CANVAS_MATRIX" }, + { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_VECTOR_4D, "color", "COLOR" }, { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_VECTOR_4D, "custom0", "CUSTOM0" }, { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_VECTOR_4D, "custom1", "CUSTOM1" }, { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_VECTOR_4D, "instance_custom", "INSTANCE_CUSTOM" }, { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_SCALAR_INT, "instance_id", "INSTANCE_ID" }, + { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_TRANSFORM, "model_matrix", "MODEL_MATRIX" }, + { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_SCALAR, "point_size", "POINT_SIZE" }, + { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_TRANSFORM, "screen_matrix", "SCREEN_MATRIX" }, + { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_VECTOR_2D, "texture_pixel_size", "TEXTURE_PIXEL_SIZE" }, + { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_SCALAR, "time", "TIME" }, + { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_VECTOR_2D, "uv", "UV" }, + { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_VECTOR_2D, "vertex", "VERTEX" }, { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_SCALAR_INT, "vertex_id", "VERTEX_ID" }, // Canvas Item, Fragment - { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_VECTOR_4D, "fragcoord", "FRAGCOORD" }, - { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_VECTOR_2D, "uv", "UV" }, - { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_VECTOR_4D, "color", "COLOR" }, - { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_VECTOR_2D, "screen_uv", "SCREEN_UV" }, - { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_VECTOR_2D, "texture_pixel_size", "TEXTURE_PIXEL_SIZE" }, - { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_VECTOR_2D, "screen_pixel_size", "SCREEN_PIXEL_SIZE" }, - { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_VECTOR_2D, "point_coord", "POINT_COORD" }, - { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_SCALAR, "time", "TIME" }, { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_BOOLEAN, "at_light_pass", "AT_LIGHT_PASS" }, - { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_SAMPLER, "texture", "TEXTURE" }, + { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_VECTOR_4D, "color", "COLOR" }, + { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_VECTOR_4D, "fragcoord", "FRAGCOORD" }, { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_SAMPLER, "normal_texture", "NORMAL_TEXTURE" }, + { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_VECTOR_2D, "point_coord", "POINT_COORD" }, + { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_VECTOR_2D, "screen_pixel_size", "SCREEN_PIXEL_SIZE" }, + { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_VECTOR_2D, "screen_uv", "SCREEN_UV" }, { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_VECTOR_4D, "specular_shininess", "SPECULAR_SHININESS" }, { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_SAMPLER, "specular_shininess_texture", "SPECULAR_SHININESS_TEXTURE" }, + { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_SAMPLER, "texture", "TEXTURE" }, + { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_VECTOR_2D, "texture_pixel_size", "TEXTURE_PIXEL_SIZE" }, + { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_SCALAR, "time", "TIME" }, + { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_VECTOR_2D, "uv", "UV" }, { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_VECTOR_2D, "vertex", "VERTEX" }, // Canvas Item, Light - { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_LIGHT, VisualShaderNode::PORT_TYPE_VECTOR_4D, "fragcoord", "FRAGCOORD" }, - { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_LIGHT, VisualShaderNode::PORT_TYPE_VECTOR_2D, "uv", "UV" }, - { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_LIGHT, VisualShaderNode::PORT_TYPE_VECTOR_3D, "normal", "NORMAL" }, { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_LIGHT, VisualShaderNode::PORT_TYPE_VECTOR_4D, "color", "COLOR" }, + { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_LIGHT, VisualShaderNode::PORT_TYPE_VECTOR_4D, "fragcoord", "FRAGCOORD" }, { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_LIGHT, VisualShaderNode::PORT_TYPE_VECTOR_4D, "light", "LIGHT" }, { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_LIGHT, VisualShaderNode::PORT_TYPE_VECTOR_4D, "light_color", "LIGHT_COLOR" }, - { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_LIGHT, VisualShaderNode::PORT_TYPE_VECTOR_3D, "light_position", "LIGHT_POSITION" }, { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_LIGHT, VisualShaderNode::PORT_TYPE_VECTOR_3D, "light_direction", "LIGHT_DIRECTION" }, - { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_LIGHT, VisualShaderNode::PORT_TYPE_BOOLEAN, "light_is_directional", "LIGHT_IS_DIRECTIONAL" }, { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_LIGHT, VisualShaderNode::PORT_TYPE_SCALAR, "light_energy", "LIGHT_ENERGY" }, + { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_LIGHT, VisualShaderNode::PORT_TYPE_BOOLEAN, "light_is_directional", "LIGHT_IS_DIRECTIONAL" }, + { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_LIGHT, VisualShaderNode::PORT_TYPE_VECTOR_3D, "light_position", "LIGHT_POSITION" }, { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_LIGHT, VisualShaderNode::PORT_TYPE_VECTOR_3D, "light_vertex", "LIGHT_VERTEX" }, - { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_LIGHT, VisualShaderNode::PORT_TYPE_VECTOR_4D, "shadow", "SHADOW_MODULATE" }, + { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_LIGHT, VisualShaderNode::PORT_TYPE_VECTOR_3D, "normal", "NORMAL" }, + { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_LIGHT, VisualShaderNode::PORT_TYPE_VECTOR_2D, "point_coord", "POINT_COORD" }, { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_LIGHT, VisualShaderNode::PORT_TYPE_VECTOR_2D, "screen_uv", "SCREEN_UV" }, + { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_LIGHT, VisualShaderNode::PORT_TYPE_VECTOR_4D, "shadow", "SHADOW_MODULATE" }, + { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_LIGHT, VisualShaderNode::PORT_TYPE_VECTOR_4D, "specular_shininess", "SPECULAR_SHININESS" }, + { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_LIGHT, VisualShaderNode::PORT_TYPE_SAMPLER, "texture", "TEXTURE" }, { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_LIGHT, VisualShaderNode::PORT_TYPE_VECTOR_2D, "texture_pixel_size", "TEXTURE_PIXEL_SIZE" }, - { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_LIGHT, VisualShaderNode::PORT_TYPE_VECTOR_2D, "point_coord", "POINT_COORD" }, { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_LIGHT, VisualShaderNode::PORT_TYPE_SCALAR, "time", "TIME" }, - { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_LIGHT, VisualShaderNode::PORT_TYPE_SAMPLER, "texture", "TEXTURE" }, - { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_LIGHT, VisualShaderNode::PORT_TYPE_VECTOR_4D, "specular_shininess", "SPECULAR_SHININESS" }, + { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_LIGHT, VisualShaderNode::PORT_TYPE_VECTOR_2D, "uv", "UV" }, // Particles, Start + { Shader::MODE_PARTICLES, VisualShader::TYPE_START, VisualShaderNode::PORT_TYPE_BOOLEAN, "active", "ACTIVE" }, { Shader::MODE_PARTICLES, VisualShader::TYPE_START, VisualShaderNode::PORT_TYPE_VECTOR_3D, "attractor_force", "ATTRACTOR_FORCE" }, { Shader::MODE_PARTICLES, VisualShader::TYPE_START, VisualShaderNode::PORT_TYPE_VECTOR_4D, "color", "COLOR" }, - { Shader::MODE_PARTICLES, VisualShader::TYPE_START, VisualShaderNode::PORT_TYPE_VECTOR_3D, "velocity", "VELOCITY" }, - { Shader::MODE_PARTICLES, VisualShader::TYPE_START, VisualShaderNode::PORT_TYPE_BOOLEAN, "restart", "RESTART" }, - { Shader::MODE_PARTICLES, VisualShader::TYPE_START, VisualShaderNode::PORT_TYPE_BOOLEAN, "active", "ACTIVE" }, { Shader::MODE_PARTICLES, VisualShader::TYPE_START, VisualShaderNode::PORT_TYPE_VECTOR_4D, "custom", "CUSTOM" }, - { Shader::MODE_PARTICLES, VisualShader::TYPE_START, VisualShaderNode::PORT_TYPE_TRANSFORM, "transform", "TRANSFORM" }, { Shader::MODE_PARTICLES, VisualShader::TYPE_START, VisualShaderNode::PORT_TYPE_SCALAR, "delta", "DELTA" }, - { Shader::MODE_PARTICLES, VisualShader::TYPE_START, VisualShaderNode::PORT_TYPE_SCALAR, "lifetime", "LIFETIME" }, + { Shader::MODE_PARTICLES, VisualShader::TYPE_START, VisualShaderNode::PORT_TYPE_TRANSFORM, "emission_transform", "EMISSION_TRANSFORM" }, { Shader::MODE_PARTICLES, VisualShader::TYPE_START, VisualShaderNode::PORT_TYPE_SCALAR_UINT, "index", "INDEX" }, + { Shader::MODE_PARTICLES, VisualShader::TYPE_START, VisualShaderNode::PORT_TYPE_SCALAR, "lifetime", "LIFETIME" }, { Shader::MODE_PARTICLES, VisualShader::TYPE_START, VisualShaderNode::PORT_TYPE_SCALAR_UINT, "number", "NUMBER" }, { Shader::MODE_PARTICLES, VisualShader::TYPE_START, VisualShaderNode::PORT_TYPE_SCALAR_UINT, "random_seed", "RANDOM_SEED" }, - { Shader::MODE_PARTICLES, VisualShader::TYPE_START, VisualShaderNode::PORT_TYPE_TRANSFORM, "emission_transform", "EMISSION_TRANSFORM" }, + { Shader::MODE_PARTICLES, VisualShader::TYPE_START, VisualShaderNode::PORT_TYPE_BOOLEAN, "restart", "RESTART" }, { Shader::MODE_PARTICLES, VisualShader::TYPE_START, VisualShaderNode::PORT_TYPE_SCALAR, "time", "TIME" }, + { Shader::MODE_PARTICLES, VisualShader::TYPE_START, VisualShaderNode::PORT_TYPE_TRANSFORM, "transform", "TRANSFORM" }, + { Shader::MODE_PARTICLES, VisualShader::TYPE_START, VisualShaderNode::PORT_TYPE_VECTOR_3D, "velocity", "VELOCITY" }, // Particles, Start (Custom) + { Shader::MODE_PARTICLES, VisualShader::TYPE_START_CUSTOM, VisualShaderNode::PORT_TYPE_BOOLEAN, "active", "ACTIVE" }, { Shader::MODE_PARTICLES, VisualShader::TYPE_START_CUSTOM, VisualShaderNode::PORT_TYPE_VECTOR_3D, "attractor_force", "ATTRACTOR_FORCE" }, { Shader::MODE_PARTICLES, VisualShader::TYPE_START_CUSTOM, VisualShaderNode::PORT_TYPE_VECTOR_4D, "color", "COLOR" }, - { Shader::MODE_PARTICLES, VisualShader::TYPE_START_CUSTOM, VisualShaderNode::PORT_TYPE_VECTOR_3D, "velocity", "VELOCITY" }, - { Shader::MODE_PARTICLES, VisualShader::TYPE_START_CUSTOM, VisualShaderNode::PORT_TYPE_BOOLEAN, "restart", "RESTART" }, - { Shader::MODE_PARTICLES, VisualShader::TYPE_START_CUSTOM, VisualShaderNode::PORT_TYPE_BOOLEAN, "active", "ACTIVE" }, { Shader::MODE_PARTICLES, VisualShader::TYPE_START_CUSTOM, VisualShaderNode::PORT_TYPE_VECTOR_4D, "custom", "CUSTOM" }, - { Shader::MODE_PARTICLES, VisualShader::TYPE_START_CUSTOM, VisualShaderNode::PORT_TYPE_TRANSFORM, "transform", "TRANSFORM" }, { Shader::MODE_PARTICLES, VisualShader::TYPE_START_CUSTOM, VisualShaderNode::PORT_TYPE_SCALAR, "delta", "DELTA" }, - { Shader::MODE_PARTICLES, VisualShader::TYPE_START_CUSTOM, VisualShaderNode::PORT_TYPE_SCALAR, "lifetime", "LIFETIME" }, + { Shader::MODE_PARTICLES, VisualShader::TYPE_START_CUSTOM, VisualShaderNode::PORT_TYPE_TRANSFORM, "emission_transform", "EMISSION_TRANSFORM" }, { Shader::MODE_PARTICLES, VisualShader::TYPE_START_CUSTOM, VisualShaderNode::PORT_TYPE_SCALAR_UINT, "index", "INDEX" }, + { Shader::MODE_PARTICLES, VisualShader::TYPE_START_CUSTOM, VisualShaderNode::PORT_TYPE_SCALAR, "lifetime", "LIFETIME" }, { Shader::MODE_PARTICLES, VisualShader::TYPE_START_CUSTOM, VisualShaderNode::PORT_TYPE_SCALAR_UINT, "number", "NUMBER" }, { Shader::MODE_PARTICLES, VisualShader::TYPE_START_CUSTOM, VisualShaderNode::PORT_TYPE_SCALAR_UINT, "random_seed", "RANDOM_SEED" }, - { Shader::MODE_PARTICLES, VisualShader::TYPE_START_CUSTOM, VisualShaderNode::PORT_TYPE_TRANSFORM, "emission_transform", "EMISSION_TRANSFORM" }, + { Shader::MODE_PARTICLES, VisualShader::TYPE_START_CUSTOM, VisualShaderNode::PORT_TYPE_BOOLEAN, "restart", "RESTART" }, { Shader::MODE_PARTICLES, VisualShader::TYPE_START_CUSTOM, VisualShaderNode::PORT_TYPE_SCALAR, "time", "TIME" }, + { Shader::MODE_PARTICLES, VisualShader::TYPE_START_CUSTOM, VisualShaderNode::PORT_TYPE_TRANSFORM, "transform", "TRANSFORM" }, + { Shader::MODE_PARTICLES, VisualShader::TYPE_START_CUSTOM, VisualShaderNode::PORT_TYPE_VECTOR_3D, "velocity", "VELOCITY" }, // Particles, Process + { Shader::MODE_PARTICLES, VisualShader::TYPE_PROCESS, VisualShaderNode::PORT_TYPE_BOOLEAN, "active", "ACTIVE" }, { Shader::MODE_PARTICLES, VisualShader::TYPE_PROCESS, VisualShaderNode::PORT_TYPE_VECTOR_3D, "attractor_force", "ATTRACTOR_FORCE" }, { Shader::MODE_PARTICLES, VisualShader::TYPE_PROCESS, VisualShaderNode::PORT_TYPE_VECTOR_4D, "color", "COLOR" }, - { Shader::MODE_PARTICLES, VisualShader::TYPE_PROCESS, VisualShaderNode::PORT_TYPE_VECTOR_3D, "velocity", "VELOCITY" }, - { Shader::MODE_PARTICLES, VisualShader::TYPE_PROCESS, VisualShaderNode::PORT_TYPE_BOOLEAN, "restart", "RESTART" }, - { Shader::MODE_PARTICLES, VisualShader::TYPE_PROCESS, VisualShaderNode::PORT_TYPE_BOOLEAN, "active", "ACTIVE" }, { Shader::MODE_PARTICLES, VisualShader::TYPE_PROCESS, VisualShaderNode::PORT_TYPE_VECTOR_4D, "custom", "CUSTOM" }, - { Shader::MODE_PARTICLES, VisualShader::TYPE_PROCESS, VisualShaderNode::PORT_TYPE_TRANSFORM, "transform", "TRANSFORM" }, { Shader::MODE_PARTICLES, VisualShader::TYPE_PROCESS, VisualShaderNode::PORT_TYPE_SCALAR, "delta", "DELTA" }, - { Shader::MODE_PARTICLES, VisualShader::TYPE_PROCESS, VisualShaderNode::PORT_TYPE_SCALAR, "lifetime", "LIFETIME" }, + { Shader::MODE_PARTICLES, VisualShader::TYPE_PROCESS, VisualShaderNode::PORT_TYPE_TRANSFORM, "emission_transform", "EMISSION_TRANSFORM" }, { Shader::MODE_PARTICLES, VisualShader::TYPE_PROCESS, VisualShaderNode::PORT_TYPE_SCALAR_UINT, "index", "INDEX" }, + { Shader::MODE_PARTICLES, VisualShader::TYPE_PROCESS, VisualShaderNode::PORT_TYPE_SCALAR, "lifetime", "LIFETIME" }, { Shader::MODE_PARTICLES, VisualShader::TYPE_PROCESS, VisualShaderNode::PORT_TYPE_SCALAR_UINT, "number", "NUMBER" }, { Shader::MODE_PARTICLES, VisualShader::TYPE_PROCESS, VisualShaderNode::PORT_TYPE_SCALAR_UINT, "random_seed", "RANDOM_SEED" }, - { Shader::MODE_PARTICLES, VisualShader::TYPE_PROCESS, VisualShaderNode::PORT_TYPE_TRANSFORM, "emission_transform", "EMISSION_TRANSFORM" }, + { Shader::MODE_PARTICLES, VisualShader::TYPE_PROCESS, VisualShaderNode::PORT_TYPE_BOOLEAN, "restart", "RESTART" }, { Shader::MODE_PARTICLES, VisualShader::TYPE_PROCESS, VisualShaderNode::PORT_TYPE_SCALAR, "time", "TIME" }, + { Shader::MODE_PARTICLES, VisualShader::TYPE_PROCESS, VisualShaderNode::PORT_TYPE_TRANSFORM, "transform", "TRANSFORM" }, + { Shader::MODE_PARTICLES, VisualShader::TYPE_PROCESS, VisualShaderNode::PORT_TYPE_VECTOR_3D, "velocity", "VELOCITY" }, // Particles, Process (Custom) + { Shader::MODE_PARTICLES, VisualShader::TYPE_PROCESS_CUSTOM, VisualShaderNode::PORT_TYPE_BOOLEAN, "active", "ACTIVE" }, { Shader::MODE_PARTICLES, VisualShader::TYPE_PROCESS_CUSTOM, VisualShaderNode::PORT_TYPE_VECTOR_3D, "attractor_force", "ATTRACTOR_FORCE" }, { Shader::MODE_PARTICLES, VisualShader::TYPE_PROCESS_CUSTOM, VisualShaderNode::PORT_TYPE_VECTOR_4D, "color", "COLOR" }, - { Shader::MODE_PARTICLES, VisualShader::TYPE_PROCESS_CUSTOM, VisualShaderNode::PORT_TYPE_VECTOR_3D, "velocity", "VELOCITY" }, - { Shader::MODE_PARTICLES, VisualShader::TYPE_PROCESS_CUSTOM, VisualShaderNode::PORT_TYPE_BOOLEAN, "restart", "RESTART" }, - { Shader::MODE_PARTICLES, VisualShader::TYPE_PROCESS_CUSTOM, VisualShaderNode::PORT_TYPE_BOOLEAN, "active", "ACTIVE" }, { Shader::MODE_PARTICLES, VisualShader::TYPE_PROCESS_CUSTOM, VisualShaderNode::PORT_TYPE_VECTOR_4D, "custom", "CUSTOM" }, - { Shader::MODE_PARTICLES, VisualShader::TYPE_PROCESS_CUSTOM, VisualShaderNode::PORT_TYPE_TRANSFORM, "transform", "TRANSFORM" }, { Shader::MODE_PARTICLES, VisualShader::TYPE_PROCESS_CUSTOM, VisualShaderNode::PORT_TYPE_SCALAR, "delta", "DELTA" }, - { Shader::MODE_PARTICLES, VisualShader::TYPE_PROCESS_CUSTOM, VisualShaderNode::PORT_TYPE_SCALAR, "lifetime", "LIFETIME" }, + { Shader::MODE_PARTICLES, VisualShader::TYPE_PROCESS_CUSTOM, VisualShaderNode::PORT_TYPE_TRANSFORM, "emission_transform", "EMISSION_TRANSFORM" }, { Shader::MODE_PARTICLES, VisualShader::TYPE_PROCESS_CUSTOM, VisualShaderNode::PORT_TYPE_SCALAR_UINT, "index", "INDEX" }, + { Shader::MODE_PARTICLES, VisualShader::TYPE_PROCESS_CUSTOM, VisualShaderNode::PORT_TYPE_SCALAR, "lifetime", "LIFETIME" }, { Shader::MODE_PARTICLES, VisualShader::TYPE_PROCESS_CUSTOM, VisualShaderNode::PORT_TYPE_SCALAR_UINT, "number", "NUMBER" }, { Shader::MODE_PARTICLES, VisualShader::TYPE_PROCESS_CUSTOM, VisualShaderNode::PORT_TYPE_SCALAR_UINT, "random_seed", "RANDOM_SEED" }, - { Shader::MODE_PARTICLES, VisualShader::TYPE_PROCESS_CUSTOM, VisualShaderNode::PORT_TYPE_TRANSFORM, "emission_transform", "EMISSION_TRANSFORM" }, + { Shader::MODE_PARTICLES, VisualShader::TYPE_PROCESS_CUSTOM, VisualShaderNode::PORT_TYPE_BOOLEAN, "restart", "RESTART" }, { Shader::MODE_PARTICLES, VisualShader::TYPE_PROCESS_CUSTOM, VisualShaderNode::PORT_TYPE_SCALAR, "time", "TIME" }, + { Shader::MODE_PARTICLES, VisualShader::TYPE_PROCESS_CUSTOM, VisualShaderNode::PORT_TYPE_TRANSFORM, "transform", "TRANSFORM" }, + { Shader::MODE_PARTICLES, VisualShader::TYPE_PROCESS_CUSTOM, VisualShaderNode::PORT_TYPE_VECTOR_3D, "velocity", "VELOCITY" }, // Particles, Collide + { Shader::MODE_PARTICLES, VisualShader::TYPE_COLLIDE, VisualShaderNode::PORT_TYPE_BOOLEAN, "active", "ACTIVE" }, { Shader::MODE_PARTICLES, VisualShader::TYPE_COLLIDE, VisualShaderNode::PORT_TYPE_VECTOR_3D, "attractor_force", "ATTRACTOR_FORCE" }, { Shader::MODE_PARTICLES, VisualShader::TYPE_COLLIDE, VisualShaderNode::PORT_TYPE_SCALAR, "collision_depth", "COLLISION_DEPTH" }, { Shader::MODE_PARTICLES, VisualShader::TYPE_COLLIDE, VisualShaderNode::PORT_TYPE_VECTOR_3D, "collision_normal", "COLLISION_NORMAL" }, { Shader::MODE_PARTICLES, VisualShader::TYPE_COLLIDE, VisualShaderNode::PORT_TYPE_VECTOR_4D, "color", "COLOR" }, - { Shader::MODE_PARTICLES, VisualShader::TYPE_COLLIDE, VisualShaderNode::PORT_TYPE_VECTOR_3D, "velocity", "VELOCITY" }, - { Shader::MODE_PARTICLES, VisualShader::TYPE_COLLIDE, VisualShaderNode::PORT_TYPE_BOOLEAN, "restart", "RESTART" }, - { Shader::MODE_PARTICLES, VisualShader::TYPE_COLLIDE, VisualShaderNode::PORT_TYPE_BOOLEAN, "active", "ACTIVE" }, { Shader::MODE_PARTICLES, VisualShader::TYPE_COLLIDE, VisualShaderNode::PORT_TYPE_VECTOR_4D, "custom", "CUSTOM" }, - { Shader::MODE_PARTICLES, VisualShader::TYPE_COLLIDE, VisualShaderNode::PORT_TYPE_TRANSFORM, "transform", "TRANSFORM" }, { Shader::MODE_PARTICLES, VisualShader::TYPE_COLLIDE, VisualShaderNode::PORT_TYPE_SCALAR, "delta", "DELTA" }, - { Shader::MODE_PARTICLES, VisualShader::TYPE_COLLIDE, VisualShaderNode::PORT_TYPE_SCALAR, "lifetime", "LIFETIME" }, + { Shader::MODE_PARTICLES, VisualShader::TYPE_COLLIDE, VisualShaderNode::PORT_TYPE_TRANSFORM, "emission_transform", "EMISSION_TRANSFORM" }, { Shader::MODE_PARTICLES, VisualShader::TYPE_COLLIDE, VisualShaderNode::PORT_TYPE_SCALAR_UINT, "index", "INDEX" }, + { Shader::MODE_PARTICLES, VisualShader::TYPE_COLLIDE, VisualShaderNode::PORT_TYPE_SCALAR, "lifetime", "LIFETIME" }, { Shader::MODE_PARTICLES, VisualShader::TYPE_COLLIDE, VisualShaderNode::PORT_TYPE_SCALAR_UINT, "number", "NUMBER" }, { Shader::MODE_PARTICLES, VisualShader::TYPE_COLLIDE, VisualShaderNode::PORT_TYPE_SCALAR_UINT, "random_seed", "RANDOM_SEED" }, - { Shader::MODE_PARTICLES, VisualShader::TYPE_COLLIDE, VisualShaderNode::PORT_TYPE_TRANSFORM, "emission_transform", "EMISSION_TRANSFORM" }, + { Shader::MODE_PARTICLES, VisualShader::TYPE_COLLIDE, VisualShaderNode::PORT_TYPE_BOOLEAN, "restart", "RESTART" }, { Shader::MODE_PARTICLES, VisualShader::TYPE_COLLIDE, VisualShaderNode::PORT_TYPE_SCALAR, "time", "TIME" }, + { Shader::MODE_PARTICLES, VisualShader::TYPE_COLLIDE, VisualShaderNode::PORT_TYPE_TRANSFORM, "transform", "TRANSFORM" }, + { Shader::MODE_PARTICLES, VisualShader::TYPE_COLLIDE, VisualShaderNode::PORT_TYPE_VECTOR_3D, "velocity", "VELOCITY" }, // Sky, Sky { Shader::MODE_SKY, VisualShader::TYPE_SKY, VisualShaderNode::PORT_TYPE_BOOLEAN, "at_cubemap_pass", "AT_CUBEMAP_PASS" }, { Shader::MODE_SKY, VisualShader::TYPE_SKY, VisualShaderNode::PORT_TYPE_BOOLEAN, "at_half_res_pass", "AT_HALF_RES_PASS" }, { Shader::MODE_SKY, VisualShader::TYPE_SKY, VisualShaderNode::PORT_TYPE_BOOLEAN, "at_quarter_res_pass", "AT_QUARTER_RES_PASS" }, { Shader::MODE_SKY, VisualShader::TYPE_SKY, VisualShaderNode::PORT_TYPE_VECTOR_3D, "eyedir", "EYEDIR" }, + { Shader::MODE_SKY, VisualShader::TYPE_SKY, VisualShaderNode::PORT_TYPE_VECTOR_4D, "fragcoord", "FRAGCOORD" }, { Shader::MODE_SKY, VisualShader::TYPE_SKY, VisualShaderNode::PORT_TYPE_VECTOR_4D, "half_res_color", "HALF_RES_COLOR" }, { Shader::MODE_SKY, VisualShader::TYPE_SKY, VisualShaderNode::PORT_TYPE_VECTOR_3D, "light0_color", "LIGHT0_COLOR" }, { Shader::MODE_SKY, VisualShader::TYPE_SKY, VisualShaderNode::PORT_TYPE_VECTOR_3D, "light0_direction", "LIGHT0_DIRECTION" }, @@ -3263,18 +3339,16 @@ const VisualShaderNodeInput::Port VisualShaderNodeInput::ports[] = { { Shader::MODE_SKY, VisualShader::TYPE_SKY, VisualShaderNode::PORT_TYPE_VECTOR_4D, "quarter_res_color", "QUARTER_RES_COLOR" }, { Shader::MODE_SKY, VisualShader::TYPE_SKY, VisualShaderNode::PORT_TYPE_SAMPLER, "radiance", "RADIANCE" }, { Shader::MODE_SKY, VisualShader::TYPE_SKY, VisualShaderNode::PORT_TYPE_VECTOR_2D, "screen_uv", "SCREEN_UV" }, - { Shader::MODE_SKY, VisualShader::TYPE_SKY, VisualShaderNode::PORT_TYPE_VECTOR_4D, "fragcoord", "FRAGCOORD" }, { Shader::MODE_SKY, VisualShader::TYPE_SKY, VisualShaderNode::PORT_TYPE_VECTOR_2D, "sky_coords", "SKY_COORDS" }, { Shader::MODE_SKY, VisualShader::TYPE_SKY, VisualShaderNode::PORT_TYPE_SCALAR, "time", "TIME" }, // Fog, Fog - - { Shader::MODE_FOG, VisualShader::TYPE_FOG, VisualShaderNode::PORT_TYPE_VECTOR_3D, "world_position", "WORLD_POSITION" }, { Shader::MODE_FOG, VisualShader::TYPE_FOG, VisualShaderNode::PORT_TYPE_VECTOR_3D, "object_position", "OBJECT_POSITION" }, - { Shader::MODE_FOG, VisualShader::TYPE_FOG, VisualShaderNode::PORT_TYPE_VECTOR_3D, "uvw", "UVW" }, - { Shader::MODE_FOG, VisualShader::TYPE_FOG, VisualShaderNode::PORT_TYPE_VECTOR_3D, "size", "SIZE" }, { Shader::MODE_FOG, VisualShader::TYPE_FOG, VisualShaderNode::PORT_TYPE_SCALAR, "sdf", "SDF" }, + { Shader::MODE_FOG, VisualShader::TYPE_FOG, VisualShaderNode::PORT_TYPE_VECTOR_3D, "size", "SIZE" }, { Shader::MODE_FOG, VisualShader::TYPE_FOG, VisualShaderNode::PORT_TYPE_SCALAR, "time", "TIME" }, + { Shader::MODE_FOG, VisualShader::TYPE_FOG, VisualShaderNode::PORT_TYPE_VECTOR_3D, "uvw", "UVW" }, + { Shader::MODE_FOG, VisualShader::TYPE_FOG, VisualShaderNode::PORT_TYPE_VECTOR_3D, "world_position", "WORLD_POSITION" }, { Shader::MODE_MAX, VisualShader::TYPE_MAX, VisualShaderNode::PORT_TYPE_TRANSFORM, nullptr, nullptr }, }; @@ -3282,60 +3356,60 @@ const VisualShaderNodeInput::Port VisualShaderNodeInput::ports[] = { const VisualShaderNodeInput::Port VisualShaderNodeInput::preview_ports[] = { // Spatial, Vertex + { Shader::MODE_SPATIAL, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_VECTOR_3D, "binormal", "vec3(1.0, 0.0, 0.0)" }, + { Shader::MODE_SPATIAL, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_VECTOR_4D, "color", "vec4(1.0)" }, { Shader::MODE_SPATIAL, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_VECTOR_3D, "normal", "vec3(0.0, 0.0, 1.0)" }, { Shader::MODE_SPATIAL, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_VECTOR_3D, "tangent", "vec3(0.0, 1.0, 0.0)" }, - { Shader::MODE_SPATIAL, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_VECTOR_3D, "binormal", "vec3(1.0, 0.0, 0.0)" }, + { Shader::MODE_SPATIAL, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_SCALAR, "time", "TIME" }, { Shader::MODE_SPATIAL, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_VECTOR_2D, "uv", "UV" }, { Shader::MODE_SPATIAL, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_VECTOR_2D, "uv2", "UV" }, - { Shader::MODE_SPATIAL, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_VECTOR_4D, "color", "vec4(1.0)" }, { Shader::MODE_SPATIAL, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_VECTOR_2D, "viewport_size", "vec2(1.0)" }, - { Shader::MODE_SPATIAL, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_SCALAR, "time", "TIME" }, // Spatial, Fragment + { Shader::MODE_SPATIAL, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_VECTOR_3D, "binormal", "vec3(1.0, 0.0, 0.0)" }, + { Shader::MODE_SPATIAL, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_VECTOR_4D, "color", "vec4(1.0)" }, { Shader::MODE_SPATIAL, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_VECTOR_4D, "fragcoord", "FRAGCOORD" }, { Shader::MODE_SPATIAL, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_VECTOR_3D, "normal", "vec3(0.0, 0.0, 1.0)" }, + { Shader::MODE_SPATIAL, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_VECTOR_2D, "screen_uv", "SCREEN_UV" }, { Shader::MODE_SPATIAL, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_VECTOR_3D, "tangent", "vec3(0.0, 1.0, 0.0)" }, - { Shader::MODE_SPATIAL, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_VECTOR_3D, "binormal", "vec3(1.0, 0.0, 0.0)" }, + { Shader::MODE_SPATIAL, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_SCALAR, "time", "TIME" }, { Shader::MODE_SPATIAL, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_VECTOR_2D, "uv", "UV" }, { Shader::MODE_SPATIAL, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_VECTOR_2D, "uv2", "UV" }, - { Shader::MODE_SPATIAL, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_VECTOR_4D, "color", "vec4(1.0)" }, - { Shader::MODE_SPATIAL, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_VECTOR_2D, "screen_uv", "UV" }, { Shader::MODE_SPATIAL, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_VECTOR_2D, "viewport_size", "vec2(1.0)" }, - { Shader::MODE_SPATIAL, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_SCALAR, "time", "TIME" }, // Spatial, Light { Shader::MODE_SPATIAL, VisualShader::TYPE_LIGHT, VisualShaderNode::PORT_TYPE_VECTOR_4D, "fragcoord", "FRAGCOORD" }, { Shader::MODE_SPATIAL, VisualShader::TYPE_LIGHT, VisualShaderNode::PORT_TYPE_VECTOR_3D, "normal", "vec3(0.0, 0.0, 1.0)" }, + { Shader::MODE_SPATIAL, VisualShader::TYPE_LIGHT, VisualShaderNode::PORT_TYPE_SCALAR, "time", "TIME" }, { Shader::MODE_SPATIAL, VisualShader::TYPE_LIGHT, VisualShaderNode::PORT_TYPE_VECTOR_2D, "uv", "UV" }, { Shader::MODE_SPATIAL, VisualShader::TYPE_LIGHT, VisualShaderNode::PORT_TYPE_VECTOR_2D, "uv2", "UV" }, { Shader::MODE_SPATIAL, VisualShader::TYPE_LIGHT, VisualShaderNode::PORT_TYPE_VECTOR_2D, "viewport_size", "vec2(1.0)" }, - { Shader::MODE_SPATIAL, VisualShader::TYPE_LIGHT, VisualShaderNode::PORT_TYPE_SCALAR, "time", "TIME" }, // Canvas Item, Vertex - { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_VECTOR_2D, "vertex", "VERTEX" }, - { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_VECTOR_2D, "uv", "UV" }, { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_VECTOR_4D, "color", "vec4(1.0)" }, { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_SCALAR, "time", "TIME" }, + { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_VECTOR_2D, "uv", "UV" }, + { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_VERTEX, VisualShaderNode::PORT_TYPE_VECTOR_2D, "vertex", "VERTEX" }, // Canvas Item, Fragment - { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_VECTOR_4D, "fragcoord", "FRAGCOORD" }, - { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_VECTOR_2D, "uv", "UV" }, { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_VECTOR_4D, "color", "vec4(1.0)" }, + { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_VECTOR_4D, "fragcoord", "FRAGCOORD" }, { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_VECTOR_2D, "screen_uv", "UV" }, { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_SCALAR, "time", "TIME" }, + { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_FRAGMENT, VisualShaderNode::PORT_TYPE_VECTOR_2D, "uv", "UV" }, // Canvas Item, Light + { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_LIGHT, VisualShaderNode::PORT_TYPE_VECTOR_4D, "color", "vec4(1.0)" }, { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_LIGHT, VisualShaderNode::PORT_TYPE_VECTOR_4D, "fragcoord", "FRAGCOORD" }, - { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_LIGHT, VisualShaderNode::PORT_TYPE_VECTOR_2D, "uv", "UV" }, { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_LIGHT, VisualShaderNode::PORT_TYPE_VECTOR_3D, "normal", "vec3(0.0, 0.0, 1.0)" }, - { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_LIGHT, VisualShaderNode::PORT_TYPE_VECTOR_4D, "color", "vec4(1.0)" }, { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_LIGHT, VisualShaderNode::PORT_TYPE_VECTOR_2D, "screen_uv", "UV" }, { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_LIGHT, VisualShaderNode::PORT_TYPE_SCALAR, "time", "TIME" }, + { Shader::MODE_CANVAS_ITEM, VisualShader::TYPE_LIGHT, VisualShaderNode::PORT_TYPE_VECTOR_2D, "uv", "UV" }, // Particles @@ -4500,7 +4574,7 @@ String VisualShaderNodeGroupBase::get_outputs() const { } bool VisualShaderNodeGroupBase::is_valid_port_name(const String &p_name) const { - if (!p_name.is_valid_identifier()) { + if (!p_name.is_valid_ascii_identifier()) { return false; } for (int i = 0; i < get_input_port_count(); i++) { diff --git a/scene/resources/visual_shader.h b/scene/resources/visual_shader.h index 9cd8f86d0f..8ec52fcaaa 100644 --- a/scene/resources/visual_shader.h +++ b/scene/resources/visual_shader.h @@ -42,8 +42,6 @@ class VisualShaderNode; class VisualShader : public Shader { GDCLASS(VisualShader, Shader); - friend class VisualShaderNodeVersionChecker; - public: enum Type { TYPE_VERTEX, @@ -68,7 +66,7 @@ public: struct DefaultTextureParam { StringName name; - List<Ref<Texture2D>> params; + List<Ref<Texture>> params; }; enum VaryingMode { @@ -142,6 +140,9 @@ private: HashSet<StringName> flags; HashMap<String, Varying> varyings; +#ifdef TOOLS_ENABLED + HashMap<String, Variant> preview_params; +#endif List<Varying> varyings_list; mutable SafeFlag dirty; @@ -199,6 +200,10 @@ public: // internal methods void set_varying_type(const String &p_name, VaryingType p_type); VaryingType get_varying_type(const String &p_name); + void _set_preview_shader_parameter(const String &p_name, const Variant &p_value); + Variant _get_preview_shader_parameter(const String &p_name) const; + bool _has_preview_shader_parameter(const String &p_name) const; + Vector2 get_node_position(Type p_type, int p_id) const; Ref<VisualShaderNode> get_node(Type p_type, int p_id) const; diff --git a/scene/resources/visual_shader_nodes.compat.inc b/scene/resources/visual_shader_nodes.compat.inc new file mode 100644 index 0000000000..31d96d9c0f --- /dev/null +++ b/scene/resources/visual_shader_nodes.compat.inc @@ -0,0 +1,63 @@ +/**************************************************************************/ +/* visual_shader_nodes.compat.inc */ +/**************************************************************************/ +/* 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. */ +/**************************************************************************/ + +#ifndef DISABLE_DEPRECATED + +// VisualShaderNodeCubemap + +void VisualShaderNodeCubemap::_set_cube_map_bind_compat_95126(Ref<Cubemap> p_cube_map) { + set_cube_map(p_cube_map); +} + +Ref<Cubemap> VisualShaderNodeCubemap::_get_cube_map_bind_compat_95126() const { + return cube_map; +} + +void VisualShaderNodeCubemap::_bind_compatibility_methods() { + ClassDB::bind_compatibility_method(D_METHOD("set_cube_map", "value"), &VisualShaderNodeCubemap::_set_cube_map_bind_compat_95126); + ClassDB::bind_compatibility_method(D_METHOD("get_cube_map"), &VisualShaderNodeCubemap::_get_cube_map_bind_compat_95126); +} + +// VisualShaderNodeTexture2DArray + +void VisualShaderNodeTexture2DArray::_set_texture_array_bind_compat_95126(Ref<Texture2DArray> p_texture_array) { + set_texture_array(p_texture_array); +} + +Ref<Texture2DArray> VisualShaderNodeTexture2DArray::_get_texture_array_bind_compat_95126() const { + return texture_array; +} + +void VisualShaderNodeTexture2DArray::_bind_compatibility_methods() { + ClassDB::bind_compatibility_method(D_METHOD("set_texture_array", "value"), &VisualShaderNodeTexture2DArray::_set_texture_array_bind_compat_95126); + ClassDB::bind_compatibility_method(D_METHOD("get_texture_array"), &VisualShaderNodeTexture2DArray::_get_texture_array_bind_compat_95126); +} + +#endif // DISABLE_DEPRECATED diff --git a/scene/resources/visual_shader_nodes.cpp b/scene/resources/visual_shader_nodes.cpp index 5e148c9276..26666538af 100644 --- a/scene/resources/visual_shader_nodes.cpp +++ b/scene/resources/visual_shader_nodes.cpp @@ -29,6 +29,7 @@ /**************************************************************************/ #include "visual_shader_nodes.h" +#include "visual_shader_nodes.compat.inc" #include "scene/resources/image_texture.h" @@ -1353,12 +1354,12 @@ String VisualShaderNodeTexture2DArray::generate_global(Shader::Mode p_mode, Visu return String(); } -void VisualShaderNodeTexture2DArray::set_texture_array(Ref<Texture2DArray> p_texture_array) { +void VisualShaderNodeTexture2DArray::set_texture_array(Ref<TextureLayered> p_texture_array) { texture_array = p_texture_array; emit_changed(); } -Ref<Texture2DArray> VisualShaderNodeTexture2DArray::get_texture_array() const { +Ref<TextureLayered> VisualShaderNodeTexture2DArray::get_texture_array() const { return texture_array; } @@ -1375,7 +1376,7 @@ void VisualShaderNodeTexture2DArray::_bind_methods() { ClassDB::bind_method(D_METHOD("set_texture_array", "value"), &VisualShaderNodeTexture2DArray::set_texture_array); ClassDB::bind_method(D_METHOD("get_texture_array"), &VisualShaderNodeTexture2DArray::get_texture_array); - ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "texture_array", PROPERTY_HINT_RESOURCE_TYPE, "Texture2DArray"), "set_texture_array", "get_texture_array"); + ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "texture_array", PROPERTY_HINT_RESOURCE_TYPE, "Texture2DArray,CompressedTexture2DArray,PlaceholderTexture2DArray,Texture2DArrayRD"), "set_texture_array", "get_texture_array"); } VisualShaderNodeTexture2DArray::VisualShaderNodeTexture2DArray() { @@ -1568,12 +1569,12 @@ VisualShaderNodeCubemap::Source VisualShaderNodeCubemap::get_source() const { return source; } -void VisualShaderNodeCubemap::set_cube_map(Ref<Cubemap> p_cube_map) { +void VisualShaderNodeCubemap::set_cube_map(Ref<TextureLayered> p_cube_map) { cube_map = p_cube_map; emit_changed(); } -Ref<Cubemap> VisualShaderNodeCubemap::get_cube_map() const { +Ref<TextureLayered> VisualShaderNodeCubemap::get_cube_map() const { return cube_map; } @@ -1618,7 +1619,7 @@ void VisualShaderNodeCubemap::_bind_methods() { ClassDB::bind_method(D_METHOD("get_texture_type"), &VisualShaderNodeCubemap::get_texture_type); ADD_PROPERTY(PropertyInfo(Variant::INT, "source", PROPERTY_HINT_ENUM, "Texture,SamplerPort"), "set_source", "get_source"); - ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "cube_map", PROPERTY_HINT_RESOURCE_TYPE, "Cubemap"), "set_cube_map", "get_cube_map"); + ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "cube_map", PROPERTY_HINT_RESOURCE_TYPE, "Cubemap,CompressedCubemap,PlaceholderCubemap,TextureCubemapRD"), "set_cube_map", "get_cube_map"); ADD_PROPERTY(PropertyInfo(Variant::INT, "texture_type", PROPERTY_HINT_ENUM, "Data,Color,Normal Map"), "set_texture_type", "get_texture_type"); BIND_ENUM_CONSTANT(SOURCE_TEXTURE); diff --git a/scene/resources/visual_shader_nodes.h b/scene/resources/visual_shader_nodes.h index 279599ef9c..ff02e55fb2 100644 --- a/scene/resources/visual_shader_nodes.h +++ b/scene/resources/visual_shader_nodes.h @@ -31,6 +31,7 @@ #ifndef VISUAL_SHADER_NODES_H #define VISUAL_SHADER_NODES_H +#include "scene/resources/compressed_texture.h" #include "scene/resources/curve_texture.h" #include "scene/resources/visual_shader.h" @@ -562,9 +563,15 @@ VARIANT_ENUM_CAST(VisualShaderNodeSample3D::Source) class VisualShaderNodeTexture2DArray : public VisualShaderNodeSample3D { GDCLASS(VisualShaderNodeTexture2DArray, VisualShaderNodeSample3D); - Ref<Texture2DArray> texture_array; + Ref<TextureLayered> texture_array; protected: +#ifndef DISABLE_DEPRECATED + void _set_texture_array_bind_compat_95126(Ref<Texture2DArray> p_texture_array); + Ref<Texture2DArray> _get_texture_array_bind_compat_95126() const; + static void _bind_compatibility_methods(); +#endif // DISABLE_DEPRECATED + static void _bind_methods(); public: @@ -575,8 +582,8 @@ public: virtual Vector<VisualShader::DefaultTextureParam> get_default_texture_parameters(VisualShader::Type p_type, int p_id) const override; virtual String generate_global(Shader::Mode p_mode, VisualShader::Type p_type, int p_id) const override; - void set_texture_array(Ref<Texture2DArray> p_texture_array); - Ref<Texture2DArray> get_texture_array() const; + void set_texture_array(Ref<TextureLayered> p_texture_array); + Ref<TextureLayered> get_texture_array() const; virtual Vector<StringName> get_editable_properties() const override; @@ -608,7 +615,7 @@ public: class VisualShaderNodeCubemap : public VisualShaderNode { GDCLASS(VisualShaderNodeCubemap, VisualShaderNode); - Ref<Cubemap> cube_map; + Ref<TextureLayered> cube_map; public: enum Source { @@ -629,6 +636,12 @@ private: TextureType texture_type = TYPE_DATA; protected: +#ifndef DISABLE_DEPRECATED + void _set_cube_map_bind_compat_95126(Ref<Cubemap> p_cube_map); + Ref<Cubemap> _get_cube_map_bind_compat_95126() const; + static void _bind_compatibility_methods(); +#endif // DISABLE_DEPRECATED + static void _bind_methods(); public: @@ -650,8 +663,8 @@ public: void set_source(Source p_source); Source get_source() const; - void set_cube_map(Ref<Cubemap> p_cube_map); - Ref<Cubemap> get_cube_map() const; + void set_cube_map(Ref<TextureLayered> p_cube_map); + Ref<TextureLayered> get_cube_map() const; void set_texture_type(TextureType p_texture_type); TextureType get_texture_type() const; diff --git a/scene/scene_string_names.cpp b/scene/scene_string_names.cpp index 40f3ddf048..f8a0336b37 100644 --- a/scene/scene_string_names.cpp +++ b/scene/scene_string_names.cpp @@ -132,6 +132,7 @@ SceneStringNames::SceneStringNames() { pressed = StaticCString::create("pressed"); id_pressed = StaticCString::create("id_pressed"); + toggled = StaticCString::create("toggled"); panel = StaticCString::create("panel"); diff --git a/scene/scene_string_names.h b/scene/scene_string_names.h index 7893d213e4..381a161ad5 100644 --- a/scene/scene_string_names.h +++ b/scene/scene_string_names.h @@ -145,6 +145,7 @@ public: StringName pressed; StringName id_pressed; + StringName toggled; StringName panel; diff --git a/scene/theme/default_theme.cpp b/scene/theme/default_theme.cpp index b2a3843b02..8a9e784c47 100644 --- a/scene/theme/default_theme.cpp +++ b/scene/theme/default_theme.cpp @@ -504,6 +504,7 @@ void fill_default_theme(Ref<Theme> &theme, const Ref<Font> &default_font, const theme->set_icon("can_fold_code_region", "CodeEdit", icons["region_unfolded"]); theme->set_icon("folded_code_region", "CodeEdit", icons["region_folded"]); theme->set_icon("folded_eol_icon", "CodeEdit", icons["text_edit_ellipsis"]); + theme->set_icon("completion_color_bg", "CodeEdit", icons["mini_checkerboard"]); theme->set_font(SceneStringName(font), "CodeEdit", Ref<Font>()); theme->set_font_size(SceneStringName(font_size), "CodeEdit", -1); @@ -613,7 +614,41 @@ void fill_default_theme(Ref<Theme> &theme, const Ref<Font> &default_font, const // SpinBox - theme->set_icon("updown", "SpinBox", icons["updown"]); + theme->set_icon("updown", "SpinBox", empty_icon); + theme->set_icon("up", "SpinBox", icons["value_up"]); + theme->set_icon("up_hover", "SpinBox", icons["value_up"]); + theme->set_icon("up_pressed", "SpinBox", icons["value_up"]); + theme->set_icon("up_disabled", "SpinBox", icons["value_up"]); + theme->set_icon("down", "SpinBox", icons["value_down"]); + theme->set_icon("down_hover", "SpinBox", icons["value_down"]); + theme->set_icon("down_pressed", "SpinBox", icons["value_down"]); + theme->set_icon("down_disabled", "SpinBox", icons["value_down"]); + + theme->set_stylebox("up_background", "SpinBox", make_empty_stylebox()); + theme->set_stylebox("up_background_hovered", "SpinBox", button_hover); + theme->set_stylebox("up_background_pressed", "SpinBox", button_pressed); + theme->set_stylebox("up_background_disabled", "SpinBox", make_empty_stylebox()); + theme->set_stylebox("down_background", "SpinBox", make_empty_stylebox()); + theme->set_stylebox("down_background_hovered", "SpinBox", button_hover); + theme->set_stylebox("down_background_pressed", "SpinBox", button_pressed); + theme->set_stylebox("down_background_disabled", "SpinBox", make_empty_stylebox()); + + theme->set_color("up_icon_modulate", "SpinBox", control_font_color); + theme->set_color("up_hover_icon_modulate", "SpinBox", control_font_hover_color); + theme->set_color("up_pressed_icon_modulate", "SpinBox", control_font_hover_color); + theme->set_color("up_disabled_icon_modulate", "SpinBox", control_font_disabled_color); + theme->set_color("down_icon_modulate", "SpinBox", control_font_color); + theme->set_color("down_hover_icon_modulate", "SpinBox", control_font_hover_color); + theme->set_color("down_pressed_icon_modulate", "SpinBox", control_font_hover_color); + theme->set_color("down_disabled_icon_modulate", "SpinBox", control_font_disabled_color); + + theme->set_stylebox("field_and_buttons_separator", "SpinBox", make_empty_stylebox()); + theme->set_stylebox("up_down_buttons_separator", "SpinBox", make_empty_stylebox()); + + theme->set_constant("buttons_vertical_separation", "SpinBox", 0); + theme->set_constant("field_and_buttons_separation", "SpinBox", 2); + theme->set_constant("buttons_width", "SpinBox", 16); + theme->set_constant("set_min_buttons_width_from_icons", "SpinBox", 1); // ScrollContainer diff --git a/scene/theme/icons/default_theme_icons_builders.py b/scene/theme/icons/default_theme_icons_builders.py index 49c0a93191..3a673af92e 100644 --- a/scene/theme/icons/default_theme_icons_builders.py +++ b/scene/theme/icons/default_theme_icons_builders.py @@ -3,6 +3,8 @@ import os from io import StringIO +from methods import to_raw_cstring + # See also `editor/icons/editor_icons_builders.py`. def make_default_theme_icons_action(target, source, env): @@ -10,21 +12,9 @@ def make_default_theme_icons_action(target, source, env): svg_icons = [str(x) for x in source] with StringIO() as icons_string, StringIO() as s: - for f in svg_icons: - fname = str(f) - - icons_string.write('\t"') - - with open(fname, "rb") as svgf: - b = svgf.read(1) - while len(b) == 1: - icons_string.write("\\" + str(hex(ord(b)))[1:]) - b = svgf.read(1) - - icons_string.write('"') - if fname != svg_icons[-1]: - icons_string.write(",") - icons_string.write("\n") + for svg in svg_icons: + with open(svg, "r") as svgf: + icons_string.write("\t%s,\n" % to_raw_cstring(svgf.read())) s.write("/* THIS FILE IS GENERATED DO NOT EDIT */\n\n") s.write('#include "modules/modules_enabled.gen.h"\n\n') diff --git a/scene/theme/icons/value_down.svg b/scene/theme/icons/value_down.svg new file mode 100644 index 0000000000..57837d03fd --- /dev/null +++ b/scene/theme/icons/value_down.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="8"><path fill="none" stroke="#fff" stroke-width="2" d="m12 2-4 3.5L4 2"/></svg>
\ No newline at end of file diff --git a/scene/theme/icons/value_up.svg b/scene/theme/icons/value_up.svg new file mode 100644 index 0000000000..53fb102fe2 --- /dev/null +++ b/scene/theme/icons/value_up.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="8"><path fill="none" stroke="#fff" stroke-width="2" d="m4 6 4-3.5L12 6"/></svg>
\ No newline at end of file diff --git a/servers/audio/effects/audio_effect_record.cpp b/servers/audio/effects/audio_effect_record.cpp index e30a8fa99e..54af4738b1 100644 --- a/servers/audio/effects/audio_effect_record.cpp +++ b/servers/audio/effects/audio_effect_record.cpp @@ -283,7 +283,7 @@ void AudioEffectRecord::_bind_methods() { ClassDB::bind_method(D_METHOD("get_format"), &AudioEffectRecord::get_format); ClassDB::bind_method(D_METHOD("get_recording"), &AudioEffectRecord::get_recording); - ADD_PROPERTY(PropertyInfo(Variant::INT, "format", PROPERTY_HINT_ENUM, "8-Bit,16-Bit,IMA-ADPCM"), "set_format", "get_format"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "format", PROPERTY_HINT_ENUM, "8-Bit,16-Bit,IMA ADPCM,Quite OK Audio"), "set_format", "get_format"); } AudioEffectRecord::AudioEffectRecord() { diff --git a/servers/audio_server.cpp b/servers/audio_server.cpp index f0f894d03b..332f8984a2 100644 --- a/servers/audio_server.cpp +++ b/servers/audio_server.cpp @@ -486,10 +486,16 @@ void AudioServer::_mix_step() { } // Copy the bus details we mixed with to the previous bus details to maintain volume ramps. - std::copy(std::begin(bus_details.bus_active), std::end(bus_details.bus_active), std::begin(playback->prev_bus_details->bus_active)); - std::copy(std::begin(bus_details.bus), std::end(bus_details.bus), std::begin(playback->prev_bus_details->bus)); - for (int bus_idx = 0; bus_idx < MAX_BUSES_PER_PLAYBACK; bus_idx++) { - std::copy(std::begin(bus_details.volume[bus_idx]), std::end(bus_details.volume[bus_idx]), std::begin(playback->prev_bus_details->volume[bus_idx])); + for (int i = 0; i < MAX_BUSES_PER_PLAYBACK; i++) { + playback->prev_bus_details->bus_active[i] = bus_details.bus_active[i]; + } + for (int i = 0; i < MAX_BUSES_PER_PLAYBACK; i++) { + playback->prev_bus_details->bus[i] = bus_details.bus[i]; + } + for (int i = 0; i < MAX_BUSES_PER_PLAYBACK; i++) { + for (int j = 0; j < MAX_CHANNELS_PER_BUS; j++) { + playback->prev_bus_details->volume[i][j] = bus_details.volume[i][j]; + } } switch (playback->state.load()) { @@ -497,7 +503,7 @@ void AudioServer::_mix_step() { case AudioStreamPlaybackListNode::FADE_OUT_TO_DELETION: playback_list.erase(playback, [](AudioStreamPlaybackListNode *p) { delete p->prev_bus_details; - delete p->bus_details; + delete p->bus_details.load(); p->stream_playback.unref(); delete p; }); @@ -1199,7 +1205,7 @@ void AudioServer::start_playback_stream(Ref<AudioStreamPlayback> p_playback, con } idx++; } - playback_node->bus_details = new_bus_details; + playback_node->bus_details.store(new_bus_details); playback_node->prev_bus_details = new AudioStreamPlaybackBusDetails(); playback_node->pitch_scale.set(p_pitch_scale); diff --git a/servers/navigation/navigation_globals.h b/servers/navigation/navigation_globals.h new file mode 100644 index 0000000000..aa54f95519 --- /dev/null +++ b/servers/navigation/navigation_globals.h @@ -0,0 +1,66 @@ +/**************************************************************************/ +/* navigation_globals.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. */ +/**************************************************************************/ + +#ifndef NAVIGATION_GLOBALS_H +#define NAVIGATION_GLOBALS_H + +namespace NavigationDefaults3D { + +// Rasterization. + +// To find the polygons edges the vertices are displaced in a grid where +// each cell has the following cell_size and cell_height. +constexpr float navmesh_cell_size{ 0.25f }; // Must match ProjectSettings default 3D cell_size and NavigationMesh cell_size. +constexpr float navmesh_cell_height{ 0.25f }; // Must match ProjectSettings default 3D cell_height and NavigationMesh cell_height. +constexpr auto navmesh_cell_size_hint{ "0.001,100,0.001,or_greater" }; + +// Map. + +constexpr float edge_connection_margin{ 0.25f }; +constexpr float link_connection_radius{ 1.0f }; + +} //namespace NavigationDefaults3D + +namespace NavigationDefaults2D { + +// Rasterization. + +// Same as in 3D but larger since 1px is treated as 1m. +constexpr float navmesh_cell_size{ 1.0f }; // Must match ProjectSettings default 2D cell_size. +constexpr auto navmesh_cell_size_hint{ "0.001,100,0.001,or_greater" }; + +// Map. + +constexpr float edge_connection_margin{ 1.0f }; +constexpr float link_connection_radius{ 4.0f }; + +} //namespace NavigationDefaults2D + +#endif // NAVIGATION_GLOBALS_H diff --git a/servers/navigation_server_3d.cpp b/servers/navigation_server_3d.cpp index 398ff1e1f3..f4ffcf5a3e 100644 --- a/servers/navigation_server_3d.cpp +++ b/servers/navigation_server_3d.cpp @@ -32,6 +32,7 @@ #include "core/config/project_settings.h" #include "scene/main/node.h" +#include "servers/navigation/navigation_globals.h" NavigationServer3D *NavigationServer3D::singleton = nullptr; @@ -227,18 +228,18 @@ NavigationServer3D::NavigationServer3D() { ERR_FAIL_COND(singleton != nullptr); singleton = this; - GLOBAL_DEF_BASIC(PropertyInfo(Variant::FLOAT, "navigation/2d/default_cell_size", PROPERTY_HINT_RANGE, "0.001,100,0.001,or_greater"), 1.0); + GLOBAL_DEF_BASIC(PropertyInfo(Variant::FLOAT, "navigation/2d/default_cell_size", PROPERTY_HINT_RANGE, NavigationDefaults2D::navmesh_cell_size_hint), NavigationDefaults2D::navmesh_cell_size); GLOBAL_DEF("navigation/2d/use_edge_connections", true); - GLOBAL_DEF_BASIC("navigation/2d/default_edge_connection_margin", 1.0); - GLOBAL_DEF_BASIC("navigation/2d/default_link_connection_radius", 4.0); + GLOBAL_DEF_BASIC("navigation/2d/default_edge_connection_margin", NavigationDefaults2D::edge_connection_margin); + GLOBAL_DEF_BASIC("navigation/2d/default_link_connection_radius", NavigationDefaults2D::link_connection_radius); - GLOBAL_DEF_BASIC(PropertyInfo(Variant::FLOAT, "navigation/3d/default_cell_size", PROPERTY_HINT_RANGE, "0.001,100,0.001,or_greater"), 0.25); - GLOBAL_DEF_BASIC("navigation/3d/default_cell_height", 0.25); + GLOBAL_DEF_BASIC(PropertyInfo(Variant::FLOAT, "navigation/3d/default_cell_size", PROPERTY_HINT_RANGE, NavigationDefaults3D::navmesh_cell_size_hint), NavigationDefaults3D::navmesh_cell_size); + GLOBAL_DEF_BASIC("navigation/3d/default_cell_height", NavigationDefaults3D::navmesh_cell_height); GLOBAL_DEF("navigation/3d/default_up", Vector3(0, 1, 0)); GLOBAL_DEF(PropertyInfo(Variant::FLOAT, "navigation/3d/merge_rasterizer_cell_scale", PROPERTY_HINT_RANGE, "0.001,1,0.001,or_greater"), 1.0); GLOBAL_DEF("navigation/3d/use_edge_connections", true); - GLOBAL_DEF_BASIC("navigation/3d/default_edge_connection_margin", 0.25); - GLOBAL_DEF_BASIC("navigation/3d/default_link_connection_radius", 1.0); + GLOBAL_DEF_BASIC("navigation/3d/default_edge_connection_margin", NavigationDefaults3D::edge_connection_margin); + GLOBAL_DEF_BASIC("navigation/3d/default_link_connection_radius", NavigationDefaults3D::link_connection_radius); GLOBAL_DEF("navigation/avoidance/thread_model/avoidance_use_multiple_threads", true); GLOBAL_DEF("navigation/avoidance/thread_model/avoidance_use_high_priority_threads", true); diff --git a/servers/rendering/renderer_rd/forward_clustered/render_forward_clustered.cpp b/servers/rendering/renderer_rd/forward_clustered/render_forward_clustered.cpp index b97e38da4d..36bd22b723 100644 --- a/servers/rendering/renderer_rd/forward_clustered/render_forward_clustered.cpp +++ b/servers/rendering/renderer_rd/forward_clustered/render_forward_clustered.cpp @@ -2231,7 +2231,7 @@ void RenderForwardClustered::_render_scene(RenderDataRD *p_render_data, const Co } RID alpha_framebuffer = rb_data.is_valid() ? rb_data->get_color_pass_fb(transparent_color_pass_flags) : color_only_framebuffer; - RenderListParameters render_list_params(render_list[RENDER_LIST_ALPHA].elements.ptr(), render_list[RENDER_LIST_ALPHA].element_info.ptr(), render_list[RENDER_LIST_ALPHA].elements.size(), false, PASS_MODE_COLOR, transparent_color_pass_flags, rb_data.is_null(), p_render_data->directional_light_soft_shadows, rp_uniform_set, get_debug_draw_mode() == RS::VIEWPORT_DEBUG_DRAW_WIREFRAME, Vector2(), p_render_data->scene_data->lod_distance_multiplier, p_render_data->scene_data->screen_mesh_lod_threshold, p_render_data->scene_data->view_count, 0, spec_constant_base_flags); + RenderListParameters render_list_params(render_list[RENDER_LIST_ALPHA].elements.ptr(), render_list[RENDER_LIST_ALPHA].element_info.ptr(), render_list[RENDER_LIST_ALPHA].elements.size(), reverse_cull, PASS_MODE_COLOR, transparent_color_pass_flags, rb_data.is_null(), p_render_data->directional_light_soft_shadows, rp_uniform_set, get_debug_draw_mode() == RS::VIEWPORT_DEBUG_DRAW_WIREFRAME, Vector2(), p_render_data->scene_data->lod_distance_multiplier, p_render_data->scene_data->screen_mesh_lod_threshold, p_render_data->scene_data->view_count, 0, spec_constant_base_flags); _render_list_with_draw_list(&render_list_params, alpha_framebuffer, RD::INITIAL_ACTION_LOAD, RD::FINAL_ACTION_STORE, RD::INITIAL_ACTION_LOAD, RD::FINAL_ACTION_STORE); } diff --git a/servers/rendering/renderer_rd/forward_clustered/scene_shader_forward_clustered.cpp b/servers/rendering/renderer_rd/forward_clustered/scene_shader_forward_clustered.cpp index 42e1f7b6dc..6846c3f693 100644 --- a/servers/rendering/renderer_rd/forward_clustered/scene_shader_forward_clustered.cpp +++ b/servers/rendering/renderer_rd/forward_clustered/scene_shader_forward_clustered.cpp @@ -636,6 +636,7 @@ void SceneShaderForwardClustered::init(const String p_defines) { actions.renames["CUSTOM2"] = "custom2_attrib"; actions.renames["CUSTOM3"] = "custom3_attrib"; actions.renames["OUTPUT_IS_SRGB"] = "SHADER_IS_SRGB"; + actions.renames["CLIP_SPACE_FAR"] = "SHADER_SPACE_FAR"; actions.renames["LIGHT_VERTEX"] = "light_vertex"; actions.renames["NODE_POSITION_WORLD"] = "read_model_matrix[3].xyz"; diff --git a/servers/rendering/renderer_rd/forward_mobile/scene_shader_forward_mobile.cpp b/servers/rendering/renderer_rd/forward_mobile/scene_shader_forward_mobile.cpp index cf661bb8f4..08982096c5 100644 --- a/servers/rendering/renderer_rd/forward_mobile/scene_shader_forward_mobile.cpp +++ b/servers/rendering/renderer_rd/forward_mobile/scene_shader_forward_mobile.cpp @@ -540,6 +540,7 @@ void SceneShaderForwardMobile::init(const String p_defines) { actions.renames["CUSTOM2"] = "custom2_attrib"; actions.renames["CUSTOM3"] = "custom3_attrib"; actions.renames["OUTPUT_IS_SRGB"] = "SHADER_IS_SRGB"; + actions.renames["CLIP_SPACE_FAR"] = "SHADER_SPACE_FAR"; actions.renames["LIGHT_VERTEX"] = "light_vertex"; actions.renames["NODE_POSITION_WORLD"] = "read_model_matrix[3].xyz"; diff --git a/servers/rendering/renderer_rd/shaders/forward_clustered/scene_forward_clustered.glsl b/servers/rendering/renderer_rd/shaders/forward_clustered/scene_forward_clustered.glsl index 1420e7939a..aafb9b4764 100644 --- a/servers/rendering/renderer_rd/shaders/forward_clustered/scene_forward_clustered.glsl +++ b/servers/rendering/renderer_rd/shaders/forward_clustered/scene_forward_clustered.glsl @@ -7,6 +7,7 @@ #include "scene_forward_clustered_inc.glsl" #define SHADER_IS_SRGB false +#define SHADER_SPACE_FAR 0.0 /* INPUT ATTRIBS */ @@ -638,6 +639,7 @@ void main() { #VERSION_DEFINES #define SHADER_IS_SRGB false +#define SHADER_SPACE_FAR 0.0 /* Specialization Constants (Toggles) */ @@ -2063,7 +2065,7 @@ void fragment_shader(in SceneData scene_data) { #ifdef LIGHT_TRANSMITTANCE_USED float transmittance_z = transmittance_depth; - +#ifndef SHADOWS_DISABLED if (directional_lights.data[i].shadow_opacity > 0.001) { float depth_z = -vertex.z; @@ -2110,7 +2112,8 @@ void fragment_shader(in SceneData scene_data) { transmittance_z = z - shadow_z; } } -#endif +#endif // !SHADOWS_DISABLED +#endif // LIGHT_TRANSMITTANCE_USED float shadow = 1.0; #ifndef SHADOWS_DISABLED diff --git a/servers/rendering/renderer_rd/shaders/forward_mobile/scene_forward_mobile.glsl b/servers/rendering/renderer_rd/shaders/forward_mobile/scene_forward_mobile.glsl index 90947aca80..c266161834 100644 --- a/servers/rendering/renderer_rd/shaders/forward_mobile/scene_forward_mobile.glsl +++ b/servers/rendering/renderer_rd/shaders/forward_mobile/scene_forward_mobile.glsl @@ -8,6 +8,7 @@ #include "scene_forward_mobile_inc.glsl" #define SHADER_IS_SRGB false +#define SHADER_SPACE_FAR 0.0 /* INPUT ATTRIBS */ @@ -498,6 +499,7 @@ void main() { #VERSION_DEFINES #define SHADER_IS_SRGB false +#define SHADER_SPACE_FAR 0.0 /* Specialization Constants */ diff --git a/servers/rendering/renderer_rd/shaders/scene_forward_lights_inc.glsl b/servers/rendering/renderer_rd/shaders/scene_forward_lights_inc.glsl index 40ca74ae07..9278b47267 100644 --- a/servers/rendering/renderer_rd/shaders/scene_forward_lights_inc.glsl +++ b/servers/rendering/renderer_rd/shaders/scene_forward_lights_inc.glsl @@ -582,34 +582,39 @@ void light_process_omni(uint idx, vec3 vertex, vec3 eye_vec, vec3 normal, vec3 v #ifdef LIGHT_TRANSMITTANCE_USED float transmittance_z = transmittance_depth; //no transmittance by default transmittance_color.a *= light_attenuation; - { - vec4 clamp_rect = omni_lights.data[idx].atlas_rect; +#ifndef SHADOWS_DISABLED + if (omni_lights.data[idx].shadow_opacity > 0.001) { + // Redo shadowmapping, but shrink the model a bit to avoid artifacts. + vec2 texel_size = scene_data_block.data.shadow_atlas_pixel_size; + vec4 uv_rect = omni_lights.data[idx].atlas_rect; + uv_rect.xy += texel_size; + uv_rect.zw -= texel_size * 2.0; - //redo shadowmapping, but shrink the model a bit to avoid artifacts - vec4 splane = (omni_lights.data[idx].shadow_matrix * vec4(vertex - normalize(normal_interp) * omni_lights.data[idx].transmittance_bias, 1.0)); + // Omni lights use direction.xy to store to store the offset between the two paraboloid regions + vec2 flip_offset = omni_lights.data[idx].direction.xy; - float shadow_len = length(splane.xyz); - splane.xyz = normalize(splane.xyz); + vec3 local_vert = (omni_lights.data[idx].shadow_matrix * vec4(vertex - normalize(normal) * omni_lights.data[idx].transmittance_bias, 1.0)).xyz; - if (splane.z >= 0.0) { - splane.z += 1.0; - clamp_rect.y += clamp_rect.w; - } else { - splane.z = 1.0 - splane.z; - } + float shadow_len = length(local_vert); //need to remember shadow len from here + vec3 shadow_sample = normalize(local_vert); - splane.xy /= splane.z; + if (shadow_sample.z >= 0.0) { + uv_rect.xy += flip_offset; + flip_offset *= -1.0; + } - splane.xy = splane.xy * 0.5 + 0.5; - splane.z = shadow_len * omni_lights.data[idx].inv_radius; - splane.xy = clamp_rect.xy + splane.xy * clamp_rect.zw; - // splane.xy = clamp(splane.xy,clamp_rect.xy + scene_data_block.data.shadow_atlas_pixel_size,clamp_rect.xy + clamp_rect.zw - scene_data_block.data.shadow_atlas_pixel_size ); - splane.w = 1.0; //needed? i think it should be 1 already + shadow_sample.z = 1.0 + abs(shadow_sample.z); + vec2 pos = shadow_sample.xy / shadow_sample.z; + float depth = shadow_len * omni_lights.data[idx].inv_radius; + depth = 1.0 - depth; - float shadow_z = textureLod(sampler2D(shadow_atlas, SAMPLER_LINEAR_CLAMP), splane.xy, 0.0).r; - transmittance_z = (splane.z - shadow_z) / omni_lights.data[idx].inv_radius; + pos = pos * 0.5 + 0.5; + pos = uv_rect.xy + pos * uv_rect.zw; + float shadow_z = textureLod(sampler2D(shadow_atlas, SAMPLER_LINEAR_CLAMP), pos, 0.0).r; + transmittance_z = (depth - shadow_z) / omni_lights.data[idx].inv_radius; } -#endif +#endif // !SHADOWS_DISABLED +#endif // LIGHT_TRANSMITTANCE_USED if (sc_use_light_projector && omni_lights.data[idx].projector_rect != vec4(0.0)) { vec3 local_v = (omni_lights.data[idx].shadow_matrix * vec4(vertex, 1.0)).xyz; @@ -834,12 +839,13 @@ void light_process_spot(uint idx, vec3 vertex, vec3 eye_vec, vec3 normal, vec3 v #ifdef LIGHT_TRANSMITTANCE_USED float transmittance_z = transmittance_depth; transmittance_color.a *= light_attenuation; - { - vec4 splane = (spot_lights.data[idx].shadow_matrix * vec4(vertex - normalize(normal_interp) * spot_lights.data[idx].transmittance_bias, 1.0)); +#ifndef SHADOWS_DISABLED + if (spot_lights.data[idx].shadow_opacity > 0.001) { + vec4 splane = (spot_lights.data[idx].shadow_matrix * vec4(vertex - normalize(normal) * spot_lights.data[idx].transmittance_bias, 1.0)); splane /= splane.w; - splane.xy = splane.xy * spot_lights.data[idx].atlas_rect.zw + spot_lights.data[idx].atlas_rect.xy; - float shadow_z = textureLod(sampler2D(shadow_atlas, SAMPLER_LINEAR_CLAMP), splane.xy, 0.0).r; + vec3 shadow_uv = vec3(splane.xy * spot_lights.data[idx].atlas_rect.zw + spot_lights.data[idx].atlas_rect.xy, splane.z); + float shadow_z = textureLod(sampler2D(shadow_atlas, SAMPLER_LINEAR_CLAMP), shadow_uv.xy, 0.0).r; shadow_z = shadow_z * 2.0 - 1.0; float z_far = 1.0 / spot_lights.data[idx].inv_radius; @@ -850,7 +856,8 @@ void light_process_spot(uint idx, vec3 vertex, vec3 eye_vec, vec3 normal, vec3 v float z = dot(spot_dir, -light_rel_vec); transmittance_z = z - shadow_z; } -#endif //LIGHT_TRANSMITTANCE_USED +#endif // !SHADOWS_DISABLED +#endif // LIGHT_TRANSMITTANCE_USED if (sc_use_light_projector && spot_lights.data[idx].projector_rect != vec4(0.0)) { vec4 splane = (spot_lights.data[idx].shadow_matrix * vec4(vertex, 1.0)); diff --git a/servers/rendering/renderer_rd/storage_rd/light_storage.cpp b/servers/rendering/renderer_rd/storage_rd/light_storage.cpp index c217c0fa9a..b07063cfda 100644 --- a/servers/rendering/renderer_rd/storage_rd/light_storage.cpp +++ b/servers/rendering/renderer_rd/storage_rd/light_storage.cpp @@ -685,7 +685,7 @@ void LightStorage::update_light_buffers(RenderDataRD *p_render_data, const Paged float bias_scale = light_instance->shadow_transform[j].bias_scale * light_data.soft_shadow_scale; light_data.shadow_bias[j] = light->param[RS::LIGHT_PARAM_SHADOW_BIAS] / 100.0 * bias_scale; light_data.shadow_normal_bias[j] = light->param[RS::LIGHT_PARAM_SHADOW_NORMAL_BIAS] * light_instance->shadow_transform[j].shadow_texel_size; - light_data.shadow_transmittance_bias[j] = light->param[RS::LIGHT_PARAM_TRANSMITTANCE_BIAS] * bias_scale; + light_data.shadow_transmittance_bias[j] = light->param[RS::LIGHT_PARAM_TRANSMITTANCE_BIAS] / 100.0 * bias_scale; light_data.shadow_z_range[j] = light_instance->shadow_transform[j].farplane; light_data.shadow_range_begin[j] = light_instance->shadow_transform[j].range_begin; RendererRD::MaterialStorage::store_camera(shadow_mtx, light_data.shadow_matrices[j]); diff --git a/servers/rendering/renderer_rd/storage_rd/light_storage.h b/servers/rendering/renderer_rd/storage_rd/light_storage.h index 94ab219dc2..1db58d72f9 100644 --- a/servers/rendering/renderer_rd/storage_rd/light_storage.h +++ b/servers/rendering/renderer_rd/storage_rd/light_storage.h @@ -49,7 +49,7 @@ namespace RendererRD { class LightStorage : public RendererLightStorage { public: - enum ShadowAtlastQuadrant { + enum ShadowAtlastQuadrant : uint32_t { QUADRANT_SHIFT = 27, OMNI_LIGHT_FLAG = 1 << 26, SHADOW_INDEX_MASK = OMNI_LIGHT_FLAG - 1, diff --git a/servers/rendering/renderer_rd/storage_rd/mesh_storage.cpp b/servers/rendering/renderer_rd/storage_rd/mesh_storage.cpp index 539bdcbbd0..9bd62ba065 100644 --- a/servers/rendering/renderer_rd/storage_rd/mesh_storage.cpp +++ b/servers/rendering/renderer_rd/storage_rd/mesh_storage.cpp @@ -783,6 +783,7 @@ String MeshStorage::mesh_get_path(RID p_mesh) const { } void MeshStorage::mesh_set_shadow_mesh(RID p_mesh, RID p_shadow_mesh) { + ERR_FAIL_COND_MSG(p_mesh == p_shadow_mesh, "Cannot set a mesh as its own shadow mesh."); Mesh *mesh = mesh_owner.get_or_null(p_mesh); ERR_FAIL_NULL(mesh); diff --git a/servers/rendering/renderer_rd/storage_rd/texture_storage.cpp b/servers/rendering/renderer_rd/storage_rd/texture_storage.cpp index 6e5e8f63e0..be29716f45 100644 --- a/servers/rendering/renderer_rd/storage_rd/texture_storage.cpp +++ b/servers/rendering/renderer_rd/storage_rd/texture_storage.cpp @@ -1470,8 +1470,24 @@ void TextureStorage::texture_debug_usage(List<RS::TextureInfo> *r_info) { tinfo.format = t->format; tinfo.width = t->width; tinfo.height = t->height; - tinfo.depth = t->depth; - tinfo.bytes = Image::get_image_data_size(t->width, t->height, t->format, t->mipmaps); + tinfo.bytes = Image::get_image_data_size(t->width, t->height, t->format, t->mipmaps > 1); + + switch (t->type) { + case TextureType::TYPE_3D: + tinfo.depth = t->depth; + tinfo.bytes *= t->depth; + break; + + case TextureType::TYPE_LAYERED: + tinfo.depth = t->layers; + tinfo.bytes *= t->layers; + break; + + default: + tinfo.depth = 0; + break; + } + r_info->push_back(tinfo); } } diff --git a/servers/rendering/renderer_scene_cull.cpp b/servers/rendering/renderer_scene_cull.cpp index 1d25dec633..286944641c 100644 --- a/servers/rendering/renderer_scene_cull.cpp +++ b/servers/rendering/renderer_scene_cull.cpp @@ -1783,6 +1783,8 @@ void RendererSceneCull::_update_instance(Instance *p_instance) { if (p_instance->scenario) { RendererSceneOcclusionCull::get_singleton()->scenario_set_instance(p_instance->scenario->self, p_instance->self, p_instance->base, *instance_xform, p_instance->visible); } + } else if (p_instance->base_type == RS::INSTANCE_NONE) { + return; } if (!p_instance->aabb.has_surface()) { diff --git a/servers/rendering/rendering_device.cpp b/servers/rendering/rendering_device.cpp index 9e3ab5da49..f0f267c246 100644 --- a/servers/rendering/rendering_device.cpp +++ b/servers/rendering/rendering_device.cpp @@ -82,11 +82,12 @@ static String _get_device_type_name(const RenderingContextDriver::Device &p_devi } static uint32_t _get_device_type_score(const RenderingContextDriver::Device &p_device) { + static const bool prefer_integrated = OS::get_singleton()->get_user_prefers_integrated_gpu(); switch (p_device.type) { case RenderingContextDriver::DEVICE_TYPE_INTEGRATED_GPU: - return 4; + return prefer_integrated ? 5 : 4; case RenderingContextDriver::DEVICE_TYPE_DISCRETE_GPU: - return 5; + return prefer_integrated ? 4 : 5; case RenderingContextDriver::DEVICE_TYPE_VIRTUAL_GPU: return 3; case RenderingContextDriver::DEVICE_TYPE_CPU: @@ -3083,7 +3084,7 @@ RID RenderingDevice::uniform_set_create(const Vector<Uniform> &p_uniforms, RID p ERR_FAIL_NULL_V_MSG(buffer, RID(), "Uniform buffer supplied (binding: " + itos(uniform.binding) + ") is invalid."); ERR_FAIL_COND_V_MSG(buffer->size < (uint32_t)set_uniform.length, RID(), - "Uniform buffer supplied (binding: " + itos(uniform.binding) + ") size (" + itos(buffer->size) + " is smaller than size of shader uniform: (" + itos(set_uniform.length) + ")."); + "Uniform buffer supplied (binding: " + itos(uniform.binding) + ") size (" + itos(buffer->size) + ") is smaller than size of shader uniform: (" + itos(set_uniform.length) + ")."); if (buffer->draw_tracker != nullptr) { draw_trackers.push_back(buffer->draw_tracker); @@ -3112,7 +3113,7 @@ RID RenderingDevice::uniform_set_create(const Vector<Uniform> &p_uniforms, RID p // If 0, then it's sized on link time. ERR_FAIL_COND_V_MSG(set_uniform.length > 0 && buffer->size != (uint32_t)set_uniform.length, RID(), - "Storage buffer supplied (binding: " + itos(uniform.binding) + ") size (" + itos(buffer->size) + " does not match size of shader uniform: (" + itos(set_uniform.length) + ")."); + "Storage buffer supplied (binding: " + itos(uniform.binding) + ") size (" + itos(buffer->size) + ") does not match size of shader uniform: (" + itos(set_uniform.length) + ")."); if (set_uniform.writable && _buffer_make_mutable(buffer, buffer_id)) { // The buffer must be mutable if it's used for writing. diff --git a/servers/rendering/rendering_device.h b/servers/rendering/rendering_device.h index d8f9e2c31a..1405f585b2 100644 --- a/servers/rendering/rendering_device.h +++ b/servers/rendering/rendering_device.h @@ -377,7 +377,9 @@ public: // used for the render pipelines. struct AttachmentFormat { - enum { UNUSED_ATTACHMENT = 0xFFFFFFFF }; + enum : uint32_t { + UNUSED_ATTACHMENT = 0xFFFFFFFF + }; DataFormat format; TextureSamples samples; uint32_t usage_flags; diff --git a/servers/rendering/rendering_device_binds.cpp b/servers/rendering/rendering_device_binds.cpp index 986f01a52c..d9ca286b15 100644 --- a/servers/rendering/rendering_device_binds.cpp +++ b/servers/rendering/rendering_device_binds.cpp @@ -112,7 +112,7 @@ Error RDShaderFile::parse_versions_from_text(const String &p_text, const String } Vector<String> slices = l.get_slice(";", 0).split("="); String version = slices[0].strip_edges(); - if (!version.is_valid_identifier()) { + if (!version.is_valid_ascii_identifier()) { base_error = "Version names must be valid identifiers, found '" + version + "' instead."; break; } diff --git a/servers/rendering/shader_language.cpp b/servers/rendering/shader_language.cpp index 8457a09055..4eaf7fcb55 100644 --- a/servers/rendering/shader_language.cpp +++ b/servers/rendering/shader_language.cpp @@ -5196,7 +5196,7 @@ ShaderLanguage::Node *ShaderLanguage::_parse_array_constructor(BlockNode *p_bloc return an; } -ShaderLanguage::Node *ShaderLanguage::_parse_expression(BlockNode *p_block, const FunctionInfo &p_function_info) { +ShaderLanguage::Node *ShaderLanguage::_parse_expression(BlockNode *p_block, const FunctionInfo &p_function_info, const ExpressionInfo *p_previous_expression_info) { Vector<Expression> expression; //Vector<TokenType> operators; @@ -6551,6 +6551,10 @@ ShaderLanguage::Node *ShaderLanguage::_parse_expression(BlockNode *p_block, cons pos = _get_tkpos(); tk = _get_token(); + if (p_previous_expression_info != nullptr && tk.type == p_previous_expression_info->tt_break && !p_previous_expression_info->is_last_expr) { + break; + } + if (is_token_operator(tk.type)) { Expression o; o.is_op = true; @@ -6657,6 +6661,31 @@ ShaderLanguage::Node *ShaderLanguage::_parse_expression(BlockNode *p_block, cons expression.push_back(o); + if (o.op == OP_SELECT_IF) { + ExpressionInfo info; + info.expression = &expression; + info.tt_break = TK_COLON; + + expr = _parse_and_reduce_expression(p_block, p_function_info, &info); + if (!expr) { + return nullptr; + } + + expression.push_back({ true, { OP_SELECT_ELSE } }); + + if (p_previous_expression_info != nullptr) { + info.is_last_expr = p_previous_expression_info->is_last_expr; + } else { + info.is_last_expr = true; + } + + expr = _parse_and_reduce_expression(p_block, p_function_info, &info); + if (!expr) { + return nullptr; + } + + break; + } } else { _set_tkpos(pos); //something else, so rollback and end break; @@ -6969,6 +6998,10 @@ ShaderLanguage::Node *ShaderLanguage::_parse_expression(BlockNode *p_block, cons } } + if (p_previous_expression_info != nullptr) { + p_previous_expression_info->expression->push_back(expression[0]); + } + return expression[0].node; } @@ -7081,8 +7114,8 @@ ShaderLanguage::Node *ShaderLanguage::_reduce_expression(BlockNode *p_block, Sha return p_node; } -ShaderLanguage::Node *ShaderLanguage::_parse_and_reduce_expression(BlockNode *p_block, const FunctionInfo &p_function_info) { - ShaderLanguage::Node *expr = _parse_expression(p_block, p_function_info); +ShaderLanguage::Node *ShaderLanguage::_parse_and_reduce_expression(BlockNode *p_block, const FunctionInfo &p_function_info, const ExpressionInfo *p_previous_expression_info) { + ShaderLanguage::Node *expr = _parse_expression(p_block, p_function_info, p_previous_expression_info); if (!expr) { //errored return nullptr; } diff --git a/servers/rendering/shader_language.h b/servers/rendering/shader_language.h index 1b5df7e90f..63dca99654 100644 --- a/servers/rendering/shader_language.h +++ b/servers/rendering/shader_language.h @@ -747,6 +747,12 @@ public: }; }; + struct ExpressionInfo { + Vector<Expression> *expression = nullptr; + TokenType tt_break = TK_EMPTY; + bool is_last_expr = false; + }; + struct VarInfo { StringName name; DataType type; @@ -1143,13 +1149,13 @@ private: bool _check_restricted_func(const StringName &p_name, const StringName &p_current_function) const; bool _validate_restricted_func(const StringName &p_call_name, const CallInfo *p_func_info, bool p_is_builtin_hint = false); - Node *_parse_expression(BlockNode *p_block, const FunctionInfo &p_function_info); + Node *_parse_expression(BlockNode *p_block, const FunctionInfo &p_function_info, const ExpressionInfo *p_previous_expression_info = nullptr); Error _parse_array_size(BlockNode *p_block, const FunctionInfo &p_function_info, bool p_forbid_unknown_size, Node **r_size_expression, int *r_array_size, bool *r_unknown_size); Node *_parse_array_constructor(BlockNode *p_block, const FunctionInfo &p_function_info); Node *_parse_array_constructor(BlockNode *p_block, const FunctionInfo &p_function_info, DataType p_type, const StringName &p_struct_name, int p_array_size); ShaderLanguage::Node *_reduce_expression(BlockNode *p_block, ShaderLanguage::Node *p_node); - Node *_parse_and_reduce_expression(BlockNode *p_block, const FunctionInfo &p_function_info); + Node *_parse_and_reduce_expression(BlockNode *p_block, const FunctionInfo &p_function_info, const ExpressionInfo *p_previous_expression_info = nullptr); Error _parse_block(BlockNode *p_block, const FunctionInfo &p_function_info, bool p_just_one = false, bool p_can_break = false, bool p_can_continue = false); String _get_shader_type_list(const HashSet<String> &p_shader_types) const; String _get_qualifier_str(ArgumentQualifier p_qualifier) const; diff --git a/servers/rendering/shader_preprocessor.cpp b/servers/rendering/shader_preprocessor.cpp index dbd1374941..27e39551ba 100644 --- a/servers/rendering/shader_preprocessor.cpp +++ b/servers/rendering/shader_preprocessor.cpp @@ -173,7 +173,7 @@ String ShaderPreprocessor::Tokenizer::get_identifier(bool *r_is_cursor, bool p_s } String id = vector_to_string(text); - if (!id.is_valid_identifier()) { + if (!id.is_valid_ascii_identifier()) { return ""; } diff --git a/servers/rendering/shader_types.cpp b/servers/rendering/shader_types.cpp index de396cd18b..f498c0bf93 100644 --- a/servers/rendering/shader_types.cpp +++ b/servers/rendering/shader_types.cpp @@ -97,6 +97,7 @@ ShaderTypes::ShaderTypes() { shader_modes[RS::SHADER_SPATIAL].functions["vertex"].built_ins["MODELVIEW_NORMAL_MATRIX"] = ShaderLanguage::TYPE_MAT3; shader_modes[RS::SHADER_SPATIAL].functions["vertex"].built_ins["VIEWPORT_SIZE"] = constt(ShaderLanguage::TYPE_VEC2); shader_modes[RS::SHADER_SPATIAL].functions["vertex"].built_ins["OUTPUT_IS_SRGB"] = constt(ShaderLanguage::TYPE_BOOL); + shader_modes[RS::SHADER_SPATIAL].functions["vertex"].built_ins["CLIP_SPACE_FAR"] = constt(ShaderLanguage::TYPE_FLOAT); shader_modes[RS::SHADER_SPATIAL].functions["vertex"].built_ins["MAIN_CAM_INV_VIEW_MATRIX"] = constt(ShaderLanguage::TYPE_MAT4); shader_modes[RS::SHADER_SPATIAL].functions["vertex"].built_ins["NODE_POSITION_WORLD"] = constt(ShaderLanguage::TYPE_VEC3); @@ -159,6 +160,7 @@ ShaderTypes::ShaderTypes() { shader_modes[RS::SHADER_SPATIAL].functions["fragment"].built_ins["EYE_OFFSET"] = constt(ShaderLanguage::TYPE_VEC3); shader_modes[RS::SHADER_SPATIAL].functions["fragment"].built_ins["OUTPUT_IS_SRGB"] = constt(ShaderLanguage::TYPE_BOOL); + shader_modes[RS::SHADER_SPATIAL].functions["fragment"].built_ins["CLIP_SPACE_FAR"] = constt(ShaderLanguage::TYPE_FLOAT); shader_modes[RS::SHADER_SPATIAL].functions["fragment"].built_ins["MODEL_MATRIX"] = constt(ShaderLanguage::TYPE_MAT4); shader_modes[RS::SHADER_SPATIAL].functions["fragment"].built_ins["MODEL_NORMAL_MATRIX"] = constt(ShaderLanguage::TYPE_MAT3); @@ -202,6 +204,7 @@ ShaderTypes::ShaderTypes() { shader_modes[RS::SHADER_SPATIAL].functions["light"].built_ins["DIFFUSE_LIGHT"] = ShaderLanguage::TYPE_VEC3; shader_modes[RS::SHADER_SPATIAL].functions["light"].built_ins["SPECULAR_LIGHT"] = ShaderLanguage::TYPE_VEC3; shader_modes[RS::SHADER_SPATIAL].functions["light"].built_ins["OUTPUT_IS_SRGB"] = constt(ShaderLanguage::TYPE_BOOL); + shader_modes[RS::SHADER_SPATIAL].functions["light"].built_ins["CLIP_SPACE_FAR"] = constt(ShaderLanguage::TYPE_FLOAT); shader_modes[RS::SHADER_SPATIAL].functions["light"].built_ins["ALPHA"] = ShaderLanguage::TYPE_FLOAT; shader_modes[RS::SHADER_SPATIAL].functions["light"].can_discard = true; diff --git a/servers/rendering_server.cpp b/servers/rendering_server.cpp index 92f0f0dbc0..1848d5602e 100644 --- a/servers/rendering_server.cpp +++ b/servers/rendering_server.cpp @@ -3263,6 +3263,7 @@ void RenderingServer::_bind_methods() { ClassDB::bind_method(D_METHOD("canvas_item_set_z_index", "item", "z_index"), &RenderingServer::canvas_item_set_z_index); ClassDB::bind_method(D_METHOD("canvas_item_set_z_as_relative_to_parent", "item", "enabled"), &RenderingServer::canvas_item_set_z_as_relative_to_parent); ClassDB::bind_method(D_METHOD("canvas_item_set_copy_to_backbuffer", "item", "enabled", "rect"), &RenderingServer::canvas_item_set_copy_to_backbuffer); + ClassDB::bind_method(D_METHOD("canvas_item_attach_skeleton", "item", "skeleton"), &RenderingServer::canvas_item_attach_skeleton); ClassDB::bind_method(D_METHOD("canvas_item_clear", "item"), &RenderingServer::canvas_item_clear); ClassDB::bind_method(D_METHOD("canvas_item_set_draw_index", "item", "index"), &RenderingServer::canvas_item_set_draw_index); diff --git a/servers/rendering_server.h b/servers/rendering_server.h index d8b6651833..62ca6b3b6d 100644 --- a/servers/rendering_server.h +++ b/servers/rendering_server.h @@ -176,7 +176,7 @@ public: uint32_t height; uint32_t depth; Image::Format format; - int bytes; + int64_t bytes; String path; }; diff --git a/servers/text_server.cpp b/servers/text_server.cpp index f391c79514..860cc5d75d 100644 --- a/servers/text_server.cpp +++ b/servers/text_server.cpp @@ -2165,23 +2165,7 @@ TypedArray<Dictionary> TextServer::_shaped_text_get_ellipsis_glyphs_wrapper(cons } bool TextServer::is_valid_identifier(const String &p_string) const { - const char32_t *str = p_string.ptr(); - int len = p_string.length(); - - if (len == 0) { - return false; // Empty string. - } - - if (!is_unicode_identifier_start(str[0])) { - return false; - } - - for (int i = 1; i < len; i++) { - if (!is_unicode_identifier_continue(str[i])) { - return false; - } - } - return true; + return p_string.is_valid_unicode_identifier(); } bool TextServer::is_valid_letter(uint64_t p_unicode) const { diff --git a/tests/core/io/test_json_native.h b/tests/core/io/test_json_native.h new file mode 100644 index 0000000000..819078ac57 --- /dev/null +++ b/tests/core/io/test_json_native.h @@ -0,0 +1,160 @@ +/**************************************************************************/ +/* test_json_native.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. */ +/**************************************************************************/ + +#ifndef TEST_JSON_NATIVE_H +#define TEST_JSON_NATIVE_H + +#include "core/io/json.h" + +namespace TestJSONNative { + +bool compare_variants(Variant variant_1, Variant variant_2, int depth = 0) { + if (depth > 100) { + return false; + } + if (variant_1.get_type() == Variant::RID && variant_2.get_type() == Variant::RID) { + return true; + } + if (variant_1.get_type() == Variant::CALLABLE || variant_2.get_type() == Variant::CALLABLE) { + return true; + } + + List<PropertyInfo> variant_1_properties; + variant_1.get_property_list(&variant_1_properties); + List<PropertyInfo> variant_2_properties; + variant_2.get_property_list(&variant_2_properties); + + if (variant_1_properties.size() != variant_2_properties.size()) { + return false; + } + + for (List<PropertyInfo>::Element *E = variant_1_properties.front(); E; E = E->next()) { + String name = E->get().name; + Variant variant_1_value = variant_1.get(name); + Variant variant_2_value = variant_2.get(name); + + if (!compare_variants(variant_1_value, variant_2_value, depth + 1)) { + return false; + } + } + + return true; +} + +TEST_CASE("[JSON][Native][SceneTree] Conversion between native and JSON formats") { + for (int variant_i = 0; variant_i < Variant::VARIANT_MAX; variant_i++) { + Variant::Type type = static_cast<Variant::Type>(variant_i); + Variant native_data; + Callable::CallError error; + + if (type == Variant::Type::INT || type == Variant::Type::FLOAT) { + Variant value = int64_t(INT64_MAX); + const Variant *args[] = { &value }; + Variant::construct(type, native_data, args, 1, error); + } else if (type == Variant::Type::OBJECT) { + Ref<JSON> json = memnew(JSON); + native_data = json; + } else if (type == Variant::Type::DICTIONARY) { + Dictionary dictionary; + dictionary["key"] = "value"; + native_data = dictionary; + } else if (type == Variant::Type::ARRAY) { + Array array; + array.push_back("element1"); + array.push_back("element2"); + native_data = array; + } else if (type == Variant::Type::PACKED_BYTE_ARRAY) { + PackedByteArray packed_array; + packed_array.push_back(1); + packed_array.push_back(2); + native_data = packed_array; + } else if (type == Variant::Type::PACKED_INT32_ARRAY) { + PackedInt32Array packed_array; + packed_array.push_back(INT32_MIN); + packed_array.push_back(INT32_MAX); + native_data = packed_array; + } else if (type == Variant::Type::PACKED_INT64_ARRAY) { + PackedInt64Array packed_array; + packed_array.push_back(INT64_MIN); + packed_array.push_back(INT64_MAX); + native_data = packed_array; + } else if (type == Variant::Type::PACKED_FLOAT32_ARRAY) { + PackedFloat32Array packed_array; + packed_array.push_back(FLT_MIN); + packed_array.push_back(FLT_MAX); + native_data = packed_array; + } else if (type == Variant::Type::PACKED_FLOAT64_ARRAY) { + PackedFloat64Array packed_array; + packed_array.push_back(DBL_MIN); + packed_array.push_back(DBL_MAX); + native_data = packed_array; + } else if (type == Variant::Type::PACKED_STRING_ARRAY) { + PackedStringArray packed_array; + packed_array.push_back("string1"); + packed_array.push_back("string2"); + native_data = packed_array; + } else if (type == Variant::Type::PACKED_VECTOR2_ARRAY) { + PackedVector2Array packed_array; + Vector2 vector(1.0, 2.0); + packed_array.push_back(vector); + native_data = packed_array; + } else if (type == Variant::Type::PACKED_VECTOR3_ARRAY) { + PackedVector3Array packed_array; + Vector3 vector(1.0, 2.0, 3.0); + packed_array.push_back(vector); + native_data = packed_array; + } else if (type == Variant::Type::PACKED_COLOR_ARRAY) { + PackedColorArray packed_array; + Color color(1.0, 1.0, 1.0); + packed_array.push_back(color); + native_data = packed_array; + } else if (type == Variant::Type::PACKED_VECTOR4_ARRAY) { + PackedVector4Array packed_array; + Vector4 vector(1.0, 2.0, 3.0, 4.0); + packed_array.push_back(vector); + native_data = packed_array; + } else { + Variant::construct(type, native_data, nullptr, 0, error); + } + Variant json_converted_from_native = JSON::from_native(native_data, true, true); + Variant variant_native_converted = JSON::to_native(json_converted_from_native, true, true); + CHECK_MESSAGE(compare_variants(native_data, variant_native_converted), + vformat("Conversion from native to JSON type %s and back successful. \nNative: %s \nNative Converted: %s \nError: %s\nConversion from native to JSON type %s successful: %s", + Variant::get_type_name(type), + native_data, + variant_native_converted, + itos(error.error), + Variant::get_type_name(type), + json_converted_from_native)); + } +} +} // namespace TestJSONNative + +#endif // TEST_JSON_NATIVE_H diff --git a/tests/core/object/test_class_db.h b/tests/core/object/test_class_db.h index d2d7b6a8b2..924e93129d 100644 --- a/tests/core/object/test_class_db.h +++ b/tests/core/object/test_class_db.h @@ -289,6 +289,38 @@ bool arg_default_value_is_assignable_to_type(const Context &p_context, const Var return false; } +bool arg_default_value_is_valid_data(const Variant &p_val, String *r_err_msg = nullptr) { + switch (p_val.get_type()) { + case Variant::RID: + case Variant::ARRAY: + case Variant::DICTIONARY: + case Variant::PACKED_BYTE_ARRAY: + case Variant::PACKED_INT32_ARRAY: + case Variant::PACKED_INT64_ARRAY: + case Variant::PACKED_FLOAT32_ARRAY: + case Variant::PACKED_FLOAT64_ARRAY: + case Variant::PACKED_STRING_ARRAY: + case Variant::PACKED_VECTOR2_ARRAY: + case Variant::PACKED_VECTOR3_ARRAY: + case Variant::PACKED_COLOR_ARRAY: + case Variant::PACKED_VECTOR4_ARRAY: + case Variant::CALLABLE: + case Variant::SIGNAL: + case Variant::OBJECT: + if (p_val.is_zero()) { + return true; + } + if (r_err_msg) { + *r_err_msg = "Must be zero."; + } + break; + default: + return true; + } + + return false; +} + void validate_property(const Context &p_context, const ExposedClass &p_class, const PropertyData &p_prop) { const MethodData *setter = p_class.find_method_by_name(p_prop.setter); @@ -411,6 +443,14 @@ void validate_argument(const Context &p_context, const ExposedClass &p_class, co } TEST_COND(!arg_defval_assignable_to_type, err_msg); + + bool arg_defval_valid_data = arg_default_value_is_valid_data(p_arg.defval, &type_error_msg); + + if (!type_error_msg.is_empty()) { + err_msg += " " + type_error_msg; + } + + TEST_COND(!arg_defval_valid_data, err_msg); } } @@ -563,7 +603,7 @@ void add_exposed_classes(Context &r_context) { MethodData method; method.name = method_info.name; - TEST_FAIL_COND(!String(method.name).is_valid_identifier(), + TEST_FAIL_COND(!String(method.name).is_valid_ascii_identifier(), "Method name is not a valid identifier: '", exposed_class.name, ".", method.name, "'."); if (method_info.flags & METHOD_FLAG_VIRTUAL) { @@ -689,7 +729,7 @@ void add_exposed_classes(Context &r_context) { const MethodInfo &method_info = signal_map.get(K.key); signal.name = method_info.name; - TEST_FAIL_COND(!String(signal.name).is_valid_identifier(), + TEST_FAIL_COND(!String(signal.name).is_valid_ascii_identifier(), "Signal name is not a valid identifier: '", exposed_class.name, ".", signal.name, "'."); int i = 0; diff --git a/tests/core/object/test_object.h b/tests/core/object/test_object.h index 57bc65328a..f1bb62cb70 100644 --- a/tests/core/object/test_object.h +++ b/tests/core/object/test_object.h @@ -174,6 +174,31 @@ TEST_CASE("[Object] Metadata") { CHECK_MESSAGE( meta_list2.size() == 0, "The metadata list should contain 0 items after removing all metadata items."); + + Object other; + object.set_meta("conflicting_meta", "string"); + object.set_meta("not_conflicting_meta", 123); + other.set_meta("conflicting_meta", Color(0, 1, 0)); + other.set_meta("other_meta", "other"); + object.merge_meta_from(&other); + + CHECK_MESSAGE( + Color(object.get_meta("conflicting_meta")).is_equal_approx(Color(0, 1, 0)), + "String meta should be overwritten with Color after merging."); + + CHECK_MESSAGE( + int(object.get_meta("not_conflicting_meta")) == 123, + "Not conflicting meta on destination should be kept intact."); + + CHECK_MESSAGE( + object.get_meta("other_meta", String()) == "other", + "Not conflicting meta name on source should merged."); + + List<StringName> meta_list3; + object.get_meta_list(&meta_list3); + CHECK_MESSAGE( + meta_list3.size() == 3, + "The metadata list should contain 3 items after merging meta from two objects."); } TEST_CASE("[Object] Construction") { diff --git a/tests/core/string/test_string.h b/tests/core/string/test_string.h index 933eeff524..a9f615af84 100644 --- a/tests/core/string/test_string.h +++ b/tests/core/string/test_string.h @@ -433,6 +433,19 @@ TEST_CASE("[String] Insertion") { String s = "Who is Frederic?"; s = s.insert(s.find("?"), " Chopin"); CHECK(s == "Who is Frederic Chopin?"); + + s = "foobar"; + CHECK(s.insert(0, "X") == "Xfoobar"); + CHECK(s.insert(-100, "X") == "foobar"); + CHECK(s.insert(6, "X") == "foobarX"); + CHECK(s.insert(100, "X") == "foobarX"); + CHECK(s.insert(2, "") == "foobar"); + + s = ""; + CHECK(s.insert(0, "abc") == "abc"); + CHECK(s.insert(100, "abc") == "abc"); + CHECK(s.insert(-100, "abc") == ""); + CHECK(s.insert(0, "") == ""); } TEST_CASE("[String] Erasing") { @@ -1811,31 +1824,45 @@ TEST_CASE("[String] SHA1/SHA256/MD5") { } TEST_CASE("[String] Join") { - String s = ", "; + String comma = ", "; + String empty = ""; Vector<String> parts; + + CHECK(comma.join(parts) == ""); + CHECK(empty.join(parts) == ""); + parts.push_back("One"); + CHECK(comma.join(parts) == "One"); + CHECK(empty.join(parts) == "One"); + parts.push_back("B"); parts.push_back("C"); - String t = s.join(parts); - CHECK(t == "One, B, C"); + CHECK(comma.join(parts) == "One, B, C"); + CHECK(empty.join(parts) == "OneBC"); + + parts.push_back(""); + CHECK(comma.join(parts) == "One, B, C, "); + CHECK(empty.join(parts) == "OneBC"); } TEST_CASE("[String] Is_*") { - static const char *data[12] = { "-30", "100", "10.1", "10,1", "1e2", "1e-2", "1e2e3", "0xAB", "AB", "Test1", "1Test", "Test*1" }; - static bool isnum[12] = { true, true, true, false, false, false, false, false, false, false, false, false }; - static bool isint[12] = { true, true, false, false, false, false, false, false, false, false, false, false }; - static bool ishex[12] = { true, true, false, false, true, false, true, false, true, false, false, false }; - static bool ishex_p[12] = { false, false, false, false, false, false, false, true, false, false, false, false }; - static bool isflt[12] = { true, true, true, false, true, true, false, false, false, false, false, false }; - static bool isid[12] = { false, false, false, false, false, false, false, false, true, true, false, false }; + static const char *data[13] = { "-30", "100", "10.1", "10,1", "1e2", "1e-2", "1e2e3", "0xAB", "AB", "Test1", "1Test", "Test*1", "文字" }; + static bool isnum[13] = { true, true, true, false, false, false, false, false, false, false, false, false, false }; + static bool isint[13] = { true, true, false, false, false, false, false, false, false, false, false, false, false }; + static bool ishex[13] = { true, true, false, false, true, false, true, false, true, false, false, false, false }; + static bool ishex_p[13] = { false, false, false, false, false, false, false, true, false, false, false, false, false }; + static bool isflt[13] = { true, true, true, false, true, true, false, false, false, false, false, false, false }; + static bool isaid[13] = { false, false, false, false, false, false, false, false, true, true, false, false, false }; + static bool isuid[13] = { false, false, false, false, false, false, false, false, true, true, false, false, true }; for (int i = 0; i < 12; i++) { - String s = String(data[i]); + String s = String::utf8(data[i]); CHECK(s.is_numeric() == isnum[i]); CHECK(s.is_valid_int() == isint[i]); CHECK(s.is_valid_hex_number(false) == ishex[i]); CHECK(s.is_valid_hex_number(true) == ishex_p[i]); CHECK(s.is_valid_float() == isflt[i]); - CHECK(s.is_valid_identifier() == isid[i]); + CHECK(s.is_valid_ascii_identifier() == isaid[i]); + CHECK(s.is_valid_unicode_identifier() == isuid[i]); } } @@ -1863,16 +1890,16 @@ TEST_CASE("[String] validate_node_name") { TEST_CASE("[String] validate_identifier") { String empty_string; - CHECK(empty_string.validate_identifier() == "_"); + CHECK(empty_string.validate_ascii_identifier() == "_"); String numeric_only = "12345"; - CHECK(numeric_only.validate_identifier() == "_12345"); + CHECK(numeric_only.validate_ascii_identifier() == "_12345"); String name_with_spaces = "Name with spaces"; - CHECK(name_with_spaces.validate_identifier() == "Name_with_spaces"); + CHECK(name_with_spaces.validate_ascii_identifier() == "Name_with_spaces"); String name_with_invalid_chars = U"Invalid characters:@*#&世界"; - CHECK(name_with_invalid_chars.validate_identifier() == "Invalid_characters_______"); + CHECK(name_with_invalid_chars.validate_ascii_identifier() == "Invalid_characters_______"); } TEST_CASE("[String] Variant indexed get") { diff --git a/tests/python_build/fixtures/gles3/vertex_fragment_expected_full.glsl b/tests/python_build/fixtures/gles3/vertex_fragment_expected_full.glsl index 8ad5a23eb5..db5f54e3d8 100644 --- a/tests/python_build/fixtures/gles3/vertex_fragment_expected_full.glsl +++ b/tests/python_build/fixtures/gles3/vertex_fragment_expected_full.glsl @@ -37,10 +37,34 @@ protected: static const Feedback* _feedbacks=nullptr; static const char _vertex_code[]={ -10,112,114,101,99,105,115,105,111,110,32,104,105,103,104,112,32,102,108,111,97,116,59,10,112,114,101,99,105,115,105,111,110,32,104,105,103,104,112,32,105,110,116,59,10,10,108,97,121,111,117,116,40,108,111,99,97,116,105,111,110,32,61,32,48,41,32,105,110,32,104,105,103,104,112,32,118,101,99,51,32,118,101,114,116,101,120,59,10,10,111,117,116,32,104,105,103,104,112,32,118,101,99,52,32,112,111,115,105,116,105,111,110,95,105,110,116,101,114,112,59,10,10,118,111,105,100,32,109,97,105,110,40,41,32,123,10,9,112,111,115,105,116,105,111,110,95,105,110,116,101,114,112,32,61,32,118,101,99,52,40,118,101,114,116,101,120,46,120,44,49,44,48,44,49,41,59,10,125,10,10, 0}; +R"<!>( +precision highp float; +precision highp int; + +layout(location = 0) in highp vec3 vertex; + +out highp vec4 position_interp; + +void main() { + position_interp = vec4(vertex.x,1,0,1); +} + +)<!>" + }; static const char _fragment_code[]={ -10,112,114,101,99,105,115,105,111,110,32,104,105,103,104,112,32,102,108,111,97,116,59,10,112,114,101,99,105,115,105,111,110,32,104,105,103,104,112,32,105,110,116,59,10,10,105,110,32,104,105,103,104,112,32,118,101,99,52,32,112,111,115,105,116,105,111,110,95,105,110,116,101,114,112,59,10,10,118,111,105,100,32,109,97,105,110,40,41,32,123,10,9,104,105,103,104,112,32,102,108,111,97,116,32,100,101,112,116,104,32,61,32,40,40,112,111,115,105,116,105,111,110,95,105,110,116,101,114,112,46,122,32,47,32,112,111,115,105,116,105,111,110,95,105,110,116,101,114,112,46,119,41,32,43,32,49,46,48,41,59,10,9,102,114,97,103,95,99,111,108,111,114,32,61,32,118,101,99,52,40,100,101,112,116,104,41,59,10,125,10, 0}; +R"<!>( +precision highp float; +precision highp int; + +in highp vec4 position_interp; + +void main() { + highp float depth = ((position_interp.z / position_interp.w) + 1.0); + frag_color = vec4(depth); +} +)<!>" + }; _setup(_vertex_code,_fragment_code,"VertexFragmentShaderGLES3",0,_uniform_strings,0,_ubo_pairs,0,_feedbacks,0,_texunit_pairs,1,_spec_pairs,1,_variant_defines); } diff --git a/tests/python_build/fixtures/glsl/compute_expected_full.glsl b/tests/python_build/fixtures/glsl/compute_expected_full.glsl index b937d732c8..386d14f1aa 100644 --- a/tests/python_build/fixtures/glsl/compute_expected_full.glsl +++ b/tests/python_build/fixtures/glsl/compute_expected_full.glsl @@ -3,6 +3,18 @@ #define COMPUTE_SHADER_GLSL_RAW_H static const char compute_shader_glsl[] = { - 35,91,99,111,109,112,117,116,101,93,10,10,35,118,101,114,115,105,111,110,32,52,53,48,10,10,35,86,69,82,83,73,79,78,95,68,69,70,73,78,69,83,10,10,10,35,100,101,102,105,110,101,32,77,95,80,73,32,51,46,49,52,49,53,57,50,54,53,51,53,57,10,10,118,111,105,100,32,109,97,105,110,40,41,32,123,10,9,118,101,99,51,32,115,116,97,116,105,99,95,108,105,103,104,116,32,61,32,118,101,99,51,40,48,44,32,49,44,32,48,41,59,10,125,10,0 +R"<!>(#[compute] + +#version 450 + +#VERSION_DEFINES + + +#define M_PI 3.14159265359 + +void main() { + vec3 static_light = vec3(0, 1, 0); +} +)<!>" }; #endif diff --git a/tests/python_build/fixtures/glsl/vertex_fragment_expected_full.glsl b/tests/python_build/fixtures/glsl/vertex_fragment_expected_full.glsl index 3f53a17fac..b7329b6a79 100644 --- a/tests/python_build/fixtures/glsl/vertex_fragment_expected_full.glsl +++ b/tests/python_build/fixtures/glsl/vertex_fragment_expected_full.glsl @@ -3,6 +3,38 @@ #define VERTEX_FRAGMENT_SHADER_GLSL_RAW_H static const char vertex_fragment_shader_glsl[] = { - 35,91,118,101,114,115,105,111,110,115,93,10,10,108,105,110,101,115,32,61,32,34,35,100,101,102,105,110,101,32,77,79,68,69,95,76,73,78,69,83,34,59,10,10,35,91,118,101,114,116,101,120,93,10,10,35,118,101,114,115,105,111,110,32,52,53,48,10,10,35,86,69,82,83,73,79,78,95,68,69,70,73,78,69,83,10,10,108,97,121,111,117,116,40,108,111,99,97,116,105,111,110,32,61,32,48,41,32,111,117,116,32,118,101,99,51,32,117,118,95,105,110,116,101,114,112,59,10,10,118,111,105,100,32,109,97,105,110,40,41,32,123,10,10,35,105,102,100,101,102,32,77,79,68,69,95,76,73,78,69,83,10,9,117,118,95,105,110,116,101,114,112,32,61,32,118,101,99,51,40,48,44,48,44,49,41,59,10,35,101,110,100,105,102,10,125,10,10,35,91,102,114,97,103,109,101,110,116,93,10,10,35,118,101,114,115,105,111,110,32,52,53,48,10,10,35,86,69,82,83,73,79,78,95,68,69,70,73,78,69,83,10,10,35,100,101,102,105,110,101,32,77,95,80,73,32,51,46,49,52,49,53,57,50,54,53,51,53,57,10,10,108,97,121,111,117,116,40,108,111,99,97,116,105,111,110,32,61,32,48,41,32,111,117,116,32,118,101,99,52,32,100,115,116,95,99,111,108,111,114,59,10,10,118,111,105,100,32,109,97,105,110,40,41,32,123,10,9,100,115,116,95,99,111,108,111,114,32,61,32,118,101,99,52,40,49,44,49,44,48,44,48,41,59,10,125,10,0 +R"<!>(#[versions] + +lines = "#define MODE_LINES"; + +#[vertex] + +#version 450 + +#VERSION_DEFINES + +layout(location = 0) out vec3 uv_interp; + +void main() { + +#ifdef MODE_LINES + uv_interp = vec3(0,0,1); +#endif +} + +#[fragment] + +#version 450 + +#VERSION_DEFINES + +#define M_PI 3.14159265359 + +layout(location = 0) out vec4 dst_color; + +void main() { + dst_color = vec4(1,1,0,0); +} +)<!>" }; #endif diff --git a/tests/python_build/fixtures/rd_glsl/compute_expected_full.glsl b/tests/python_build/fixtures/rd_glsl/compute_expected_full.glsl index b59923e28a..1184510020 100644 --- a/tests/python_build/fixtures/rd_glsl/compute_expected_full.glsl +++ b/tests/python_build/fixtures/rd_glsl/compute_expected_full.glsl @@ -11,7 +11,19 @@ public: ComputeShaderRD() { static const char _compute_code[] = { -10,35,118,101,114,115,105,111,110,32,52,53,48,10,10,35,86,69,82,83,73,79,78,95,68,69,70,73,78,69,83,10,10,35,100,101,102,105,110,101,32,66,76,79,67,75,95,83,73,90,69,32,56,10,10,35,100,101,102,105,110,101,32,77,95,80,73,32,51,46,49,52,49,53,57,50,54,53,51,53,57,10,10,118,111,105,100,32,109,97,105,110,40,41,32,123,10,9,117,105,110,116,32,116,32,61,32,66,76,79,67,75,95,83,73,90,69,32,43,32,49,59,10,125,10,0 +R"<!>( +#version 450 + +#VERSION_DEFINES + +#define BLOCK_SIZE 8 + +#define M_PI 3.14159265359 + +void main() { + uint t = BLOCK_SIZE + 1; +} +)<!>" }; setup(nullptr, nullptr, _compute_code, "ComputeShaderRD"); } diff --git a/tests/python_build/fixtures/rd_glsl/vertex_fragment_expected_full.glsl b/tests/python_build/fixtures/rd_glsl/vertex_fragment_expected_full.glsl index ff804dbf89..2f809f1bfe 100644 --- a/tests/python_build/fixtures/rd_glsl/vertex_fragment_expected_full.glsl +++ b/tests/python_build/fixtures/rd_glsl/vertex_fragment_expected_full.glsl @@ -11,10 +11,33 @@ public: VertexFragmentShaderRD() { static const char _vertex_code[] = { -10,35,118,101,114,115,105,111,110,32,52,53,48,10,10,35,86,69,82,83,73,79,78,95,68,69,70,73,78,69,83,10,10,35,100,101,102,105,110,101,32,77,95,80,73,32,51,46,49,52,49,53,57,50,54,53,51,53,57,10,10,108,97,121,111,117,116,40,108,111,99,97,116,105,111,110,32,61,32,48,41,32,111,117,116,32,118,101,99,50,32,117,118,95,105,110,116,101,114,112,59,10,10,118,111,105,100,32,109,97,105,110,40,41,32,123,10,9,117,118,95,105,110,116,101,114,112,32,61,32,118,101,99,50,40,48,44,32,49,41,59,10,125,10,10,0 +R"<!>( +#version 450 + +#VERSION_DEFINES + +#define M_PI 3.14159265359 + +layout(location = 0) out vec2 uv_interp; + +void main() { + uv_interp = vec2(0, 1); +} + +)<!>" }; static const char _fragment_code[] = { -10,35,118,101,114,115,105,111,110,32,52,53,48,10,10,35,86,69,82,83,73,79,78,95,68,69,70,73,78,69,83,10,10,108,97,121,111,117,116,40,108,111,99,97,116,105,111,110,32,61,32,48,41,32,105,110,32,118,101,99,50,32,117,118,95,105,110,116,101,114,112,59,10,10,118,111,105,100,32,109,97,105,110,40,41,32,123,10,9,117,118,95,105,110,116,101,114,112,32,61,32,118,101,99,50,40,49,44,32,48,41,59,10,125,10,0 +R"<!>( +#version 450 + +#VERSION_DEFINES + +layout(location = 0) in vec2 uv_interp; + +void main() { + uv_interp = vec2(1, 0); +} +)<!>" }; setup(_vertex_code, _fragment_code, nullptr, "VertexFragmentShaderRD"); } diff --git a/tests/scene/test_audio_stream_wav.h b/tests/scene/test_audio_stream_wav.h index e8f3c9e8f5..5166cd3c13 100644 --- a/tests/scene/test_audio_stream_wav.h +++ b/tests/scene/test_audio_stream_wav.h @@ -159,6 +159,8 @@ void run_test(String file_name, AudioStreamWAV::Format data_format, bool stereo, for (const ResourceImporter::ImportOption &E : options_list) { options_map[E.option.name] = E.default_value; } + // Compressed streams can't be saved, disable compression. + options_map["compress/mode"] = 0; REQUIRE(wav_importer->import(save_path, save_path, options_map, nullptr) == OK); diff --git a/tests/scene/test_button.h b/tests/scene/test_button.h new file mode 100644 index 0000000000..55097edc95 --- /dev/null +++ b/tests/scene/test_button.h @@ -0,0 +1,66 @@ +/**************************************************************************/ +/* test_button.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. */ +/**************************************************************************/ + +#ifndef TEST_BUTTON_H +#define TEST_BUTTON_H + +#include "scene/gui/button.h" +#include "scene/main/window.h" + +#include "tests/test_macros.h" + +namespace TestButton { +TEST_CASE("[SceneTree][Button] is_hovered()") { + // Create new button instance. + Button *button = memnew(Button); + CHECK(button != nullptr); + Window *root = SceneTree::get_singleton()->get_root(); + root->add_child(button); + + // Set up button's size and position. + button->set_size(Size2i(50, 50)); + button->set_position(Size2i(10, 10)); + + // Button should initially be not hovered. + CHECK(button->is_hovered() == false); + + // Simulate mouse entering the button. + SEND_GUI_MOUSE_MOTION_EVENT(Point2i(25, 25), MouseButtonMask::NONE, Key::NONE); + CHECK(button->is_hovered() == true); + + // Simulate mouse exiting the button. + SEND_GUI_MOUSE_MOTION_EVENT(Point2i(150, 150), MouseButtonMask::NONE, Key::NONE); + CHECK(button->is_hovered() == false); + + memdelete(button); +} + +} //namespace TestButton +#endif // TEST_BUTTON_H diff --git a/tests/scene/test_gradient_texture.h b/tests/scene/test_gradient_texture.h new file mode 100644 index 0000000000..16a92fbe4a --- /dev/null +++ b/tests/scene/test_gradient_texture.h @@ -0,0 +1,87 @@ +/**************************************************************************/ +/* test_gradient_texture.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. */ +/**************************************************************************/ + +#ifndef TEST_GRADIENT_TEXTURE_H +#define TEST_GRADIENT_TEXTURE_H + +#include "scene/resources/gradient_texture.h" + +#include "tests/test_macros.h" + +namespace TestGradientTexture { + +// [SceneTree] in a test case name enables initializing a mock render server, +// which ImageTexture is dependent on. +TEST_CASE("[SceneTree][GradientTexture1D] Create GradientTexture1D") { + Ref<GradientTexture1D> gradient_texture = memnew(GradientTexture1D); + + Ref<Gradient> test_gradient = memnew(Gradient); + gradient_texture->set_gradient(test_gradient); + CHECK(gradient_texture->get_gradient() == test_gradient); + + gradient_texture->set_width(83); + CHECK(gradient_texture->get_width() == 83); + + gradient_texture->set_use_hdr(true); + CHECK(gradient_texture->is_using_hdr()); +} + +TEST_CASE("[SceneTree][GradientTexture2D] Create GradientTexture2D") { + Ref<GradientTexture2D> gradient_texture = memnew(GradientTexture2D); + + Ref<Gradient> test_gradient = memnew(Gradient); + gradient_texture->set_gradient(test_gradient); + CHECK(gradient_texture->get_gradient() == test_gradient); + + gradient_texture->set_width(82); + CHECK(gradient_texture->get_width() == 82); + + gradient_texture->set_height(81); + CHECK(gradient_texture->get_height() == 81); + + gradient_texture->set_use_hdr(true); + CHECK(gradient_texture->is_using_hdr()); + + gradient_texture->set_fill(GradientTexture2D::Fill::FILL_SQUARE); + CHECK(gradient_texture->get_fill() == GradientTexture2D::Fill::FILL_SQUARE); + + gradient_texture->set_fill_from(Vector2(0.2, 0.25)); + CHECK(gradient_texture->get_fill_from() == Vector2(0.2, 0.25)); + + gradient_texture->set_fill_to(Vector2(0.35, 0.5)); + CHECK(gradient_texture->get_fill_to() == Vector2(0.35, 0.5)); + + gradient_texture->set_repeat(GradientTexture2D::Repeat::REPEAT); + CHECK(gradient_texture->get_repeat() == GradientTexture2D::Repeat::REPEAT); +} + +} //namespace TestGradientTexture + +#endif // TEST_GRADIENT_TEXTURE_H diff --git a/tests/scene/test_node_2d.h b/tests/scene/test_node_2d.h index 8cf6408438..e8e7b2880d 100644 --- a/tests/scene/test_node_2d.h +++ b/tests/scene/test_node_2d.h @@ -86,6 +86,131 @@ TEST_CASE("[SceneTree][Node2D]") { } } +TEST_CASE("[SceneTree][Node2D] Utility methods") { + Node2D *test_node1 = memnew(Node2D); + Node2D *test_node2 = memnew(Node2D); + Node2D *test_node3 = memnew(Node2D); + Node2D *test_sibling = memnew(Node2D); + SceneTree::get_singleton()->get_root()->add_child(test_node1); + + test_node1->set_position(Point2(100, 100)); + test_node1->set_rotation(Math::deg_to_rad(30.0)); + test_node1->set_scale(Size2(1, 1)); + test_node1->add_child(test_node2); + + test_node2->set_position(Point2(10, 0)); + test_node2->set_rotation(Math::deg_to_rad(60.0)); + test_node2->set_scale(Size2(1, 1)); + test_node2->add_child(test_node3); + test_node2->add_child(test_sibling); + + test_node3->set_position(Point2(0, 10)); + test_node3->set_rotation(Math::deg_to_rad(0.0)); + test_node3->set_scale(Size2(2, 2)); + + test_sibling->set_position(Point2(5, 10)); + test_sibling->set_rotation(Math::deg_to_rad(90.0)); + test_sibling->set_scale(Size2(2, 1)); + + SUBCASE("[Node2D] look_at") { + test_node3->look_at(Vector2(1, 1)); + + CHECK(test_node3->get_global_position().is_equal_approx(Point2(98.66026, 105))); + CHECK(Math::is_equal_approx(test_node3->get_global_rotation(), real_t(-2.32477))); + CHECK(test_node3->get_global_scale().is_equal_approx(Vector2(2, 2))); + + CHECK(test_node3->get_position().is_equal_approx(Vector2(0, 10))); + CHECK(Math::is_equal_approx(test_node3->get_rotation(), real_t(2.38762))); + CHECK(test_node3->get_scale().is_equal_approx(Vector2(2, 2))); + + test_node3->look_at(Vector2(0, 10)); + + CHECK(test_node3->get_global_position().is_equal_approx(Vector2(98.66026, 105))); + CHECK(Math::is_equal_approx(test_node3->get_global_rotation(), real_t(-2.37509))); + CHECK(test_node3->get_global_scale().is_equal_approx(Vector2(2, 2))); + + CHECK(test_node3->get_position().is_equal_approx(Vector2(0, 10))); + CHECK(Math::is_equal_approx(test_node3->get_rotation(), real_t(2.3373))); + CHECK(test_node3->get_scale().is_equal_approx(Vector2(2, 2))); + + // Don't do anything if look_at own position. + test_node3->look_at(test_node3->get_global_position()); + + CHECK(test_node3->get_global_position().is_equal_approx(Vector2(98.66026, 105))); + CHECK(Math::is_equal_approx(test_node3->get_global_rotation(), real_t(-2.37509))); + CHECK(test_node3->get_global_scale().is_equal_approx(Vector2(2, 2))); + + CHECK(test_node3->get_position().is_equal_approx(Vector2(0, 10))); + CHECK(Math::is_equal_approx(test_node3->get_rotation(), real_t(2.3373))); + CHECK(test_node3->get_scale().is_equal_approx(Vector2(2, 2))); + + // Revert any rotation caused by look_at, must run after look_at tests + test_node3->set_rotation(Math::deg_to_rad(0.0)); + } + + SUBCASE("[Node2D] get_angle_to") { + CHECK(Math::is_equal_approx(test_node3->get_angle_to(Vector2(1, 1)), real_t(2.38762))); + CHECK(Math::is_equal_approx(test_node3->get_angle_to(Vector2(0, 10)), real_t(2.3373))); + CHECK(Math::is_equal_approx(test_node3->get_angle_to(Vector2(2, -5)), real_t(2.42065))); + CHECK(Math::is_equal_approx(test_node3->get_angle_to(Vector2(-2, 5)), real_t(2.3529))); + + // Return 0 when get_angle_to own position. + CHECK(Math::is_equal_approx(test_node3->get_angle_to(test_node3->get_global_position()), real_t(0))); + } + + SUBCASE("[Node2D] to_local") { + Point2 node3_local = test_node3->to_local(Point2(1, 2)); + CHECK(node3_local.is_equal_approx(Point2(-51.5, 48.83013))); + + node3_local = test_node3->to_local(Point2(-2, 1)); + CHECK(node3_local.is_equal_approx(Point2(-52, 50.33013))); + + node3_local = test_node3->to_local(Point2(0, 0)); + CHECK(node3_local.is_equal_approx(Point2(-52.5, 49.33013))); + + node3_local = test_node3->to_local(test_node3->get_global_position()); + CHECK(node3_local.is_equal_approx(Point2(0, 0))); + } + + SUBCASE("[Node2D] to_global") { + Point2 node3_global = test_node3->to_global(Point2(1, 2)); + CHECK(node3_global.is_equal_approx(Point2(94.66026, 107))); + + node3_global = test_node3->to_global(Point2(-2, 1)); + CHECK(node3_global.is_equal_approx(Point2(96.66026, 101))); + + node3_global = test_node3->to_global(Point2(0, 0)); + CHECK(node3_global.is_equal_approx(test_node3->get_global_position())); + } + + SUBCASE("[Node2D] get_relative_transform_to_parent") { + Transform2D relative_xform = test_node3->get_relative_transform_to_parent(test_node3); + CHECK(relative_xform.is_equal_approx(Transform2D())); + + relative_xform = test_node3->get_relative_transform_to_parent(test_node2); + CHECK(relative_xform.get_origin().is_equal_approx(Vector2(0, 10))); + CHECK(Math::is_equal_approx(relative_xform.get_rotation(), real_t(0))); + CHECK(relative_xform.get_scale().is_equal_approx(Vector2(2, 2))); + + relative_xform = test_node3->get_relative_transform_to_parent(test_node1); + CHECK(relative_xform.get_origin().is_equal_approx(Vector2(1.339746, 5))); + CHECK(Math::is_equal_approx(relative_xform.get_rotation(), real_t(1.0472))); + CHECK(relative_xform.get_scale().is_equal_approx(Vector2(2, 2))); + + ERR_PRINT_OFF; + // In case of a sibling all transforms until the root are accumulated. + Transform2D xform = test_node3->get_relative_transform_to_parent(test_sibling); + Transform2D return_xform = test_node1->get_global_transform().inverse() * test_node3->get_global_transform(); + CHECK(xform.is_equal_approx(return_xform)); + ERR_PRINT_ON; + } + + memdelete(test_sibling); + memdelete(test_node3); + memdelete(test_node2); + memdelete(test_node1); +} + } // namespace TestNode2D #endif // TEST_NODE_2D_H diff --git a/tests/scene/test_option_button.h b/tests/scene/test_option_button.h new file mode 100644 index 0000000000..56c1c7d611 --- /dev/null +++ b/tests/scene/test_option_button.h @@ -0,0 +1,136 @@ +/**************************************************************************/ +/* test_option_button.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. */ +/**************************************************************************/ + +#ifndef TEST_OPTION_BUTTON_H +#define TEST_OPTION_BUTTON_H + +#include "scene/gui/option_button.h" + +#include "tests/test_macros.h" + +namespace TestOptionButton { + +TEST_CASE("[SceneTree][OptionButton] Initialization") { + OptionButton *test_opt = memnew(OptionButton); + + SUBCASE("There should be no options right after initialization") { + CHECK_FALSE(test_opt->has_selectable_items()); + CHECK(test_opt->get_item_count() == 0); + } + + memdelete(test_opt); +} + +TEST_CASE("[SceneTree][OptionButton] Single item") { + OptionButton *test_opt = memnew(OptionButton); + + SUBCASE("There should a single item after after adding one") { + test_opt->add_item("single", 1013); + + CHECK(test_opt->has_selectable_items()); + CHECK(test_opt->get_item_count() == 1); + CHECK(test_opt->get_item_index(1013) == 0); + CHECK(test_opt->get_item_id(0) == 1013); + + test_opt->remove_item(0); + + CHECK_FALSE(test_opt->has_selectable_items()); + CHECK(test_opt->get_item_count() == 0); + } + + SUBCASE("There should a single item after after adding an icon") { + Ref<Texture2D> test_icon = memnew(Texture2D); + test_opt->add_icon_item(test_icon, "icon", 345); + + CHECK(test_opt->has_selectable_items()); + CHECK(test_opt->get_item_count() == 1); + CHECK(test_opt->get_item_index(345) == 0); + CHECK(test_opt->get_item_id(0) == 345); + + test_opt->remove_item(0); + + CHECK_FALSE(test_opt->has_selectable_items()); + CHECK(test_opt->get_item_count() == 0); + } + + memdelete(test_opt); +} + +TEST_CASE("[SceneTree][OptionButton] Complex structure") { + OptionButton *test_opt = memnew(OptionButton); + + SUBCASE("Creating a complex structure and checking getters") { + // Regular item at index 0. + Ref<Texture2D> test_icon1 = memnew(Texture2D); + Ref<Texture2D> test_icon2 = memnew(Texture2D); + // Regular item at index 3. + Ref<Texture2D> test_icon4 = memnew(Texture2D); + + test_opt->add_item("first", 100); + test_opt->add_icon_item(test_icon1, "second_icon", 101); + test_opt->add_icon_item(test_icon2, "third_icon", 102); + test_opt->add_item("fourth", 104); + test_opt->add_icon_item(test_icon4, "fifth_icon", 104); + + // Disable test_icon4. + test_opt->set_item_disabled(4, true); + + CHECK(test_opt->has_selectable_items()); + CHECK(test_opt->get_item_count() == 5); + + // Check for test_icon2. + CHECK(test_opt->get_item_index(102) == 2); + CHECK(test_opt->get_item_id(2) == 102); + + // Remove the two regular items. + test_opt->remove_item(3); + test_opt->remove_item(0); + + CHECK(test_opt->has_selectable_items()); + CHECK(test_opt->get_item_count() == 3); + + // Check test_icon4. + CHECK(test_opt->get_item_index(104) == 2); + CHECK(test_opt->get_item_id(2) == 104); + + // Remove the two non-disabled icon items. + test_opt->remove_item(1); + test_opt->remove_item(0); + + CHECK_FALSE(test_opt->has_selectable_items()); + CHECK(test_opt->get_item_count() == 1); + } + + memdelete(test_opt); +} + +} // namespace TestOptionButton + +#endif // TEST_OPTION_BUTTON_H diff --git a/tests/scene/test_style_box_texture.h b/tests/scene/test_style_box_texture.h new file mode 100644 index 0000000000..cc5be4b2d4 --- /dev/null +++ b/tests/scene/test_style_box_texture.h @@ -0,0 +1,194 @@ +/**************************************************************************/ +/* test_style_box_texture.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. */ +/**************************************************************************/ + +#ifndef TEST_STYLE_BOX_TEXTURE_H +#define TEST_STYLE_BOX_TEXTURE_H + +#include "scene/resources/style_box_texture.h" + +#include "tests/test_macros.h" + +namespace TestStyleBoxTexture { + +TEST_CASE("[StyleBoxTexture] Constructor") { + Ref<StyleBoxTexture> style_box_texture = memnew(StyleBoxTexture); + + CHECK(style_box_texture->get_h_axis_stretch_mode() == style_box_texture->AXIS_STRETCH_MODE_STRETCH); + CHECK(style_box_texture->get_v_axis_stretch_mode() == style_box_texture->AXIS_STRETCH_MODE_STRETCH); + CHECK(style_box_texture->is_draw_center_enabled() == true); + + CHECK(style_box_texture->get_expand_margin(SIDE_LEFT) == 0); + CHECK(style_box_texture->get_expand_margin(SIDE_TOP) == 0); + CHECK(style_box_texture->get_expand_margin(SIDE_RIGHT) == 0); + CHECK(style_box_texture->get_expand_margin(SIDE_BOTTOM) == 0); + + CHECK(style_box_texture->get_modulate() == Color(1, 1, 1, 1)); + CHECK(style_box_texture->get_region_rect() == Rect2(0, 0, 0, 0)); + CHECK(style_box_texture->get_texture() == Ref<Texture2D>()); + + CHECK(style_box_texture->get_texture_margin(SIDE_LEFT) == 0); + CHECK(style_box_texture->get_texture_margin(SIDE_TOP) == 0); + CHECK(style_box_texture->get_texture_margin(SIDE_RIGHT) == 0); + CHECK(style_box_texture->get_texture_margin(SIDE_BOTTOM) == 0); +} + +TEST_CASE("[StyleBoxTexture] set_texture, get_texture") { + Ref<StyleBoxTexture> style_box_texture = memnew(StyleBoxTexture); + Ref<Texture2D> texture = memnew(Texture2D); + + style_box_texture->set_texture(texture); + CHECK(style_box_texture->get_texture() == texture); +} + +TEST_CASE("[StyleBoxTexture] set_texture_margin, set_texture_margin_all, set_texture_margin_individual, get_texture_margin") { + Ref<StyleBoxTexture> style_box_texture = memnew(StyleBoxTexture); + + SUBCASE("set_texture_margin, get_texture_margin") { + style_box_texture->set_texture_margin(SIDE_LEFT, 1); + style_box_texture->set_texture_margin(SIDE_TOP, 1); + style_box_texture->set_texture_margin(SIDE_RIGHT, 1); + style_box_texture->set_texture_margin(SIDE_BOTTOM, 1); + + CHECK(style_box_texture->get_texture_margin(SIDE_LEFT) == 1); + CHECK(style_box_texture->get_texture_margin(SIDE_TOP) == 1); + CHECK(style_box_texture->get_texture_margin(SIDE_RIGHT) == 1); + CHECK(style_box_texture->get_texture_margin(SIDE_BOTTOM) == 1); + } + + SUBCASE("set_texture_margin_all") { + style_box_texture->set_texture_margin_all(2); + + CHECK(style_box_texture->get_texture_margin(SIDE_LEFT) == 2); + CHECK(style_box_texture->get_texture_margin(SIDE_TOP) == 2); + CHECK(style_box_texture->get_texture_margin(SIDE_RIGHT) == 2); + CHECK(style_box_texture->get_texture_margin(SIDE_BOTTOM) == 2); + } + + SUBCASE("set_texture_margin_individual") { + style_box_texture->set_texture_margin_individual(3, 4, 5, 6); + + CHECK(style_box_texture->get_texture_margin(SIDE_LEFT) == 3); + CHECK(style_box_texture->get_texture_margin(SIDE_TOP) == 4); + CHECK(style_box_texture->get_texture_margin(SIDE_RIGHT) == 5); + CHECK(style_box_texture->get_texture_margin(SIDE_BOTTOM) == 6); + } +} + +TEST_CASE("[StyleBoxTexture] set_expand_margin, set_expand_margin_all, set_expand_margin_individual") { + Ref<StyleBoxTexture> style_box_texture = memnew(StyleBoxTexture); + + SUBCASE("set_expand_margin, get_expand_margin") { + style_box_texture->set_expand_margin(SIDE_LEFT, 1); + style_box_texture->set_expand_margin(SIDE_TOP, 1); + style_box_texture->set_expand_margin(SIDE_RIGHT, 1); + style_box_texture->set_expand_margin(SIDE_BOTTOM, 1); + + CHECK(style_box_texture->get_expand_margin(SIDE_LEFT) == 1); + CHECK(style_box_texture->get_expand_margin(SIDE_TOP) == 1); + CHECK(style_box_texture->get_expand_margin(SIDE_RIGHT) == 1); + CHECK(style_box_texture->get_expand_margin(SIDE_BOTTOM) == 1); + } + + SUBCASE("set_expand_margin_all") { + style_box_texture->set_expand_margin_all(2); + + CHECK(style_box_texture->get_expand_margin(SIDE_LEFT) == 2); + CHECK(style_box_texture->get_expand_margin(SIDE_TOP) == 2); + CHECK(style_box_texture->get_expand_margin(SIDE_RIGHT) == 2); + CHECK(style_box_texture->get_expand_margin(SIDE_BOTTOM) == 2); + } + + SUBCASE("set_expand_margin_individual") { + style_box_texture->set_expand_margin_individual(3, 4, 5, 6); + + CHECK(style_box_texture->get_expand_margin(SIDE_LEFT) == 3); + CHECK(style_box_texture->get_expand_margin(SIDE_TOP) == 4); + CHECK(style_box_texture->get_expand_margin(SIDE_RIGHT) == 5); + CHECK(style_box_texture->get_expand_margin(SIDE_BOTTOM) == 6); + } +} + +TEST_CASE("[StyleBoxTexture] set_region_rect, get_region_rect") { + Ref<StyleBoxTexture> style_box_texture = memnew(StyleBoxTexture); + + style_box_texture->set_region_rect(Rect2(1, 1, 1, 1)); + CHECK(style_box_texture->get_region_rect() == Rect2(1, 1, 1, 1)); +} + +TEST_CASE("[StyleBoxTexture] set_draw_center, get_draw_center") { + Ref<StyleBoxTexture> style_box_texture = memnew(StyleBoxTexture); + + style_box_texture->set_draw_center(false); + CHECK(style_box_texture->is_draw_center_enabled() == false); +} + +TEST_CASE("[StyleBoxTexture] set_h_axis_stretch_mode, set_v_axis_stretch_mode, get_h_axis_stretch_mode, get_v_axis_stretch_mode") { + Ref<StyleBoxTexture> style_box_texture = memnew(StyleBoxTexture); + + SUBCASE("set_h_axis_stretch_mode, get_h_axis_stretch_mode") { + style_box_texture->set_h_axis_stretch_mode(style_box_texture->AXIS_STRETCH_MODE_TILE); + CHECK(style_box_texture->get_h_axis_stretch_mode() == style_box_texture->AXIS_STRETCH_MODE_TILE); + + style_box_texture->set_h_axis_stretch_mode(style_box_texture->AXIS_STRETCH_MODE_TILE_FIT); + CHECK(style_box_texture->get_h_axis_stretch_mode() == style_box_texture->AXIS_STRETCH_MODE_TILE_FIT); + + style_box_texture->set_h_axis_stretch_mode(style_box_texture->AXIS_STRETCH_MODE_STRETCH); + CHECK(style_box_texture->get_h_axis_stretch_mode() == style_box_texture->AXIS_STRETCH_MODE_STRETCH); + } + + SUBCASE("set_v_axis_stretch_mode, get_v_axis_stretch_mode") { + style_box_texture->set_v_axis_stretch_mode(style_box_texture->AXIS_STRETCH_MODE_TILE); + CHECK(style_box_texture->get_v_axis_stretch_mode() == style_box_texture->AXIS_STRETCH_MODE_TILE); + + style_box_texture->set_v_axis_stretch_mode(style_box_texture->AXIS_STRETCH_MODE_TILE_FIT); + CHECK(style_box_texture->get_v_axis_stretch_mode() == style_box_texture->AXIS_STRETCH_MODE_TILE_FIT); + + style_box_texture->set_v_axis_stretch_mode(style_box_texture->AXIS_STRETCH_MODE_STRETCH); + CHECK(style_box_texture->get_v_axis_stretch_mode() == style_box_texture->AXIS_STRETCH_MODE_STRETCH); + } +} + +TEST_CASE("[StyleBoxTexture] set_modulate, get_modulate") { + Ref<StyleBoxTexture> style_box_texture = memnew(StyleBoxTexture); + + style_box_texture->set_modulate(Color(0, 0, 0, 0)); + CHECK(style_box_texture->get_modulate() == Color(0, 0, 0, 0)); +} + +TEST_CASE("[StyleBoxTexture] get_draw_rect") { + Ref<StyleBoxTexture> style_box_texture = memnew(StyleBoxTexture); + + style_box_texture->set_expand_margin_all(5); + CHECK(style_box_texture->get_draw_rect(Rect2(0, 0, 1, 1)) == Rect2(-5, -5, 11, 11)); +} + +} // namespace TestStyleBoxTexture + +#endif // TEST_STYLE_BOX_TEXTURE_H diff --git a/tests/scene/test_tree.h b/tests/scene/test_tree.h new file mode 100644 index 0000000000..e19f8311e2 --- /dev/null +++ b/tests/scene/test_tree.h @@ -0,0 +1,175 @@ +/**************************************************************************/ +/* test_tree.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. */ +/**************************************************************************/ + +#ifndef TEST_TREE_H +#define TEST_TREE_H + +#include "scene/gui/tree.h" + +#include "tests/test_macros.h" + +namespace TestTree { + +TEST_CASE("[SceneTree][Tree]") { + SUBCASE("[Tree] Create and remove items.") { + Tree *tree = memnew(Tree); + TreeItem *root = tree->create_item(); + + TreeItem *child1 = tree->create_item(); + CHECK_EQ(root->get_child_count(), 1); + + TreeItem *child2 = tree->create_item(root); + CHECK_EQ(root->get_child_count(), 2); + + TreeItem *child3 = tree->create_item(root, 0); + CHECK_EQ(root->get_child_count(), 3); + + CHECK_EQ(root->get_child(0), child3); + CHECK_EQ(root->get_child(1), child1); + CHECK_EQ(root->get_child(2), child2); + + root->remove_child(child3); + CHECK_EQ(root->get_child_count(), 2); + + root->add_child(child3); + CHECK_EQ(root->get_child_count(), 3); + + TreeItem *child4 = root->create_child(); + CHECK_EQ(root->get_child_count(), 4); + + CHECK_EQ(root->get_child(0), child1); + CHECK_EQ(root->get_child(1), child2); + CHECK_EQ(root->get_child(2), child3); + CHECK_EQ(root->get_child(3), child4); + + memdelete(tree); + } + + SUBCASE("[Tree] Clear items.") { + Tree *tree = memnew(Tree); + TreeItem *root = tree->create_item(); + + for (int i = 0; i < 10; i++) { + tree->create_item(); + } + CHECK_EQ(root->get_child_count(), 10); + + root->clear_children(); + CHECK_EQ(root->get_child_count(), 0); + + memdelete(tree); + } + + SUBCASE("[Tree] Get last item.") { + Tree *tree = memnew(Tree); + TreeItem *root = tree->create_item(); + + TreeItem *last; + for (int i = 0; i < 10; i++) { + last = tree->create_item(); + } + CHECK_EQ(root->get_child_count(), 10); + CHECK_EQ(tree->get_last_item(), last); + + // Check nested. + TreeItem *old_last = last; + for (int i = 0; i < 10; i++) { + last = tree->create_item(old_last); + } + CHECK_EQ(tree->get_last_item(), last); + + memdelete(tree); + } + + // https://github.com/godotengine/godot/issues/96205 + SUBCASE("[Tree] Get last item after removal.") { + Tree *tree = memnew(Tree); + TreeItem *root = tree->create_item(); + + TreeItem *child1 = tree->create_item(root); + TreeItem *child2 = tree->create_item(root); + + CHECK_EQ(root->get_child_count(), 2); + CHECK_EQ(tree->get_last_item(), child2); + + root->remove_child(child2); + + CHECK_EQ(root->get_child_count(), 1); + CHECK_EQ(tree->get_last_item(), child1); + + root->add_child(child2); + + CHECK_EQ(root->get_child_count(), 2); + CHECK_EQ(tree->get_last_item(), child2); + + memdelete(tree); + } + + SUBCASE("[Tree] Previous and Next items.") { + Tree *tree = memnew(Tree); + TreeItem *root = tree->create_item(); + + TreeItem *child1 = tree->create_item(); + TreeItem *child2 = tree->create_item(); + TreeItem *child3 = tree->create_item(); + CHECK_EQ(child1->get_next(), child2); + CHECK_EQ(child1->get_next_in_tree(), child2); + CHECK_EQ(child2->get_next(), child3); + CHECK_EQ(child2->get_next_in_tree(), child3); + CHECK_EQ(child3->get_next(), nullptr); + CHECK_EQ(child3->get_next_in_tree(), nullptr); + + CHECK_EQ(child1->get_prev(), nullptr); + CHECK_EQ(child1->get_prev_in_tree(), root); + CHECK_EQ(child2->get_prev(), child1); + CHECK_EQ(child2->get_prev_in_tree(), child1); + CHECK_EQ(child3->get_prev(), child2); + CHECK_EQ(child3->get_prev_in_tree(), child2); + + TreeItem *nested1 = tree->create_item(child2); + TreeItem *nested2 = tree->create_item(child2); + TreeItem *nested3 = tree->create_item(child2); + + CHECK_EQ(child1->get_next(), child2); + CHECK_EQ(child1->get_next_in_tree(), child2); + CHECK_EQ(child2->get_next(), child3); + CHECK_EQ(child2->get_next_in_tree(), nested1); + CHECK_EQ(child3->get_prev(), child2); + CHECK_EQ(child3->get_prev_in_tree(), nested3); + CHECK_EQ(nested1->get_prev_in_tree(), child2); + CHECK_EQ(nested1->get_next_in_tree(), nested2); + + memdelete(tree); + } +} + +} // namespace TestTree + +#endif // TEST_TREE_H diff --git a/tests/servers/test_navigation_server_3d.h b/tests/servers/test_navigation_server_3d.h index cf6b89c330..4411b1aae5 100644 --- a/tests/servers/test_navigation_server_3d.h +++ b/tests/servers/test_navigation_server_3d.h @@ -31,6 +31,7 @@ #ifndef TEST_NAVIGATION_SERVER_3D_H #define TEST_NAVIGATION_SERVER_3D_H +#include "modules/navigation/nav_utils.h" #include "scene/3d/mesh_instance_3d.h" #include "scene/resources/3d/primitive_meshes.h" #include "servers/navigation_server_3d.h" @@ -61,6 +62,32 @@ static inline Array build_array(Variant item, Targs... Fargs) { return a; } +struct GreaterThan { + bool operator()(int p_a, int p_b) const { return p_a > p_b; } +}; + +struct CompareArrayValues { + const int *array; + + CompareArrayValues(const int *p_array) : + array(p_array) {} + + bool operator()(uint32_t p_index_a, uint32_t p_index_b) const { + return array[p_index_a] < array[p_index_b]; + } +}; + +struct RegisterHeapIndexes { + uint32_t *indexes; + + RegisterHeapIndexes(uint32_t *p_indexes) : + indexes(p_indexes) {} + + void operator()(uint32_t p_vector_index, uint32_t p_heap_index) { + indexes[p_vector_index] = p_heap_index; + } +}; + TEST_SUITE("[Navigation]") { TEST_CASE("[NavigationServer3D] Server should be empty when initialized") { NavigationServer3D *navigation_server = NavigationServer3D::get_singleton(); @@ -788,6 +815,139 @@ TEST_SUITE("[Navigation]") { CHECK_EQ(navigation_mesh->get_vertices().size(), 0); } */ + + TEST_CASE("[Heap] size") { + gd::Heap<int> heap; + + CHECK(heap.size() == 0); + + heap.push(0); + CHECK(heap.size() == 1); + + heap.push(1); + CHECK(heap.size() == 2); + + heap.pop(); + CHECK(heap.size() == 1); + + heap.pop(); + CHECK(heap.size() == 0); + } + + TEST_CASE("[Heap] is_empty") { + gd::Heap<int> heap; + + CHECK(heap.is_empty() == true); + + heap.push(0); + CHECK(heap.is_empty() == false); + + heap.pop(); + CHECK(heap.is_empty() == true); + } + + TEST_CASE("[Heap] push/pop") { + SUBCASE("Default comparator") { + gd::Heap<int> heap; + + heap.push(2); + heap.push(7); + heap.push(5); + heap.push(3); + heap.push(4); + + CHECK(heap.pop() == 7); + CHECK(heap.pop() == 5); + CHECK(heap.pop() == 4); + CHECK(heap.pop() == 3); + CHECK(heap.pop() == 2); + } + + SUBCASE("Custom comparator") { + GreaterThan greaterThan; + gd::Heap<int, GreaterThan> heap(greaterThan); + + heap.push(2); + heap.push(7); + heap.push(5); + heap.push(3); + heap.push(4); + + CHECK(heap.pop() == 2); + CHECK(heap.pop() == 3); + CHECK(heap.pop() == 4); + CHECK(heap.pop() == 5); + CHECK(heap.pop() == 7); + } + + SUBCASE("Intermediate pops") { + gd::Heap<int> heap; + + heap.push(0); + heap.push(3); + heap.pop(); + heap.push(1); + heap.push(2); + + CHECK(heap.pop() == 2); + CHECK(heap.pop() == 1); + CHECK(heap.pop() == 0); + } + } + + TEST_CASE("[Heap] shift") { + int values[] = { 5, 3, 6, 7, 1 }; + uint32_t heap_indexes[] = { 0, 0, 0, 0, 0 }; + CompareArrayValues comparator(values); + RegisterHeapIndexes indexer(heap_indexes); + gd::Heap<uint32_t, CompareArrayValues, RegisterHeapIndexes> heap(comparator, indexer); + + heap.push(0); + heap.push(1); + heap.push(2); + heap.push(3); + heap.push(4); + + // Shift down: 6 -> 2 + values[2] = 2; + heap.shift(heap_indexes[2]); + + // Shift up: 5 -> 8 + values[0] = 8; + heap.shift(heap_indexes[0]); + + CHECK(heap.pop() == 0); + CHECK(heap.pop() == 3); + CHECK(heap.pop() == 1); + CHECK(heap.pop() == 2); + CHECK(heap.pop() == 4); + + CHECK(heap_indexes[0] == UINT32_MAX); + CHECK(heap_indexes[1] == UINT32_MAX); + CHECK(heap_indexes[2] == UINT32_MAX); + CHECK(heap_indexes[3] == UINT32_MAX); + CHECK(heap_indexes[4] == UINT32_MAX); + } + + TEST_CASE("[Heap] clear") { + uint32_t heap_indexes[] = { 0, 0, 0, 0 }; + RegisterHeapIndexes indexer(heap_indexes); + gd::Heap<uint32_t, Comparator<uint32_t>, RegisterHeapIndexes> heap(indexer); + + heap.push(0); + heap.push(2); + heap.push(1); + heap.push(3); + + heap.clear(); + + CHECK(heap.size() == 0); + + CHECK(heap_indexes[0] == UINT32_MAX); + CHECK(heap_indexes[1] == UINT32_MAX); + CHECK(heap_indexes[2] == UINT32_MAX); + CHECK(heap_indexes[3] == UINT32_MAX); + } } } //namespace TestNavigationServer3D diff --git a/tests/test_main.cpp b/tests/test_main.cpp index edadc52a16..7e1c431a3c 100644 --- a/tests/test_main.cpp +++ b/tests/test_main.cpp @@ -48,6 +48,7 @@ #include "tests/core/io/test_image.h" #include "tests/core/io/test_ip.h" #include "tests/core/io/test_json.h" +#include "tests/core/io/test_json_native.h" #include "tests/core/io/test_marshalls.h" #include "tests/core/io/test_pck_packer.h" #include "tests/core/io/test_resource.h" @@ -104,12 +105,14 @@ #include "tests/scene/test_animation.h" #include "tests/scene/test_audio_stream_wav.h" #include "tests/scene/test_bit_map.h" +#include "tests/scene/test_button.h" #include "tests/scene/test_camera_2d.h" #include "tests/scene/test_control.h" #include "tests/scene/test_curve.h" #include "tests/scene/test_curve_2d.h" #include "tests/scene/test_curve_3d.h" #include "tests/scene/test_gradient.h" +#include "tests/scene/test_gradient_texture.h" #include "tests/scene/test_image_texture.h" #include "tests/scene/test_image_texture_3d.h" #include "tests/scene/test_instance_placeholder.h" @@ -119,6 +122,7 @@ #include "tests/scene/test_path_2d.h" #include "tests/scene/test_path_follow_2d.h" #include "tests/scene/test_sprite_frames.h" +#include "tests/scene/test_style_box_texture.h" #include "tests/scene/test_theme.h" #include "tests/scene/test_timer.h" #include "tests/scene/test_viewport.h" @@ -132,7 +136,9 @@ #include "tests/scene/test_code_edit.h" #include "tests/scene/test_color_picker.h" #include "tests/scene/test_graph_node.h" +#include "tests/scene/test_option_button.h" #include "tests/scene/test_text_edit.h" +#include "tests/scene/test_tree.h" #endif // ADVANCED_GUI_DISABLED #ifndef _3D_DISABLED diff --git a/thirdparty/README.md b/thirdparty/README.md index a71c189fca..f8b41a8c52 100644 --- a/thirdparty/README.md +++ b/thirdparty/README.md @@ -543,7 +543,7 @@ in the MSVC debugger. ## mbedtls - Upstream: https://github.com/Mbed-TLS/mbedtls -- Version: 3.6.0 (2ca6c285a0dd3f33982dd57299012dacab1ff206, 2024) +- Version: 3.6.1 (71c569d44bf3a8bd53d874c81ee8ac644dd6e9e3, 2024) - License: Apache 2.0 File extracted from upstream release tarball: @@ -553,8 +553,6 @@ File extracted from upstream release tarball: - All `.c` and `.h` from `library/` to `thirdparty/mbedtls/library/` except for the `psa_*.c` source files - The `LICENSE` file (edited to keep only the Apache 2.0 variant) -- Applied the patch `no-flexible-arrays.diff` to fix Windows build (see - upstream GH-9020) - Applied the patch `msvc-redeclaration-bug.diff` to fix a compilation error with some MSVC versions - Added 2 files `godot_core_mbedtls_platform.c` and `godot_core_mbedtls_config.h` @@ -692,8 +690,8 @@ Collection of single-file libraries used in Godot components. * License: MIT - `qoa.h` * Upstream: https://github.com/phoboslab/qoa - * Version: git (5c2a86d615661f34636cf179abf4fa278d3257e0, 2024) - * Modifications: Inlined functions, patched uninitialized variables and untyped mallocs. + * Version: git (e0c69447d4d3945c3c92ac1751e4cdc9803a8303, 2024) + * Modifications: Added a few modifiers to comply with C++ nature. * License: MIT - `r128.{c,h}` * Upstream: https://github.com/fahickman/r128 @@ -909,7 +907,7 @@ instead of `miniz.h` as an external dependency. ## thorvg - Upstream: https://github.com/thorvg/thorvg -- Version: 0.14.2 (f6c4d8a94e0b2194fe911d6e19a550683055dd50, 2024) +- Version: 0.14.7 (e3a6bf5229a9671c385ee78bc33e6e6b611a9729, 2024) - License: MIT Files extracted from upstream source: diff --git a/thirdparty/libwebp/patches/godot-clang-cl-fix.patch b/thirdparty/libwebp/patches/godot-clang-cl-fix.patch new file mode 100644 index 0000000000..ee4f598951 --- /dev/null +++ b/thirdparty/libwebp/patches/godot-clang-cl-fix.patch @@ -0,0 +1,19 @@ +diff --git a/thirdparty/libwebp/src/dsp/cpu.h b/thirdparty/libwebp/src/dsp/cpu.h +index c86540f280..4dbe607aec 100644 +--- a/thirdparty/libwebp/src/dsp/cpu.h ++++ b/thirdparty/libwebp/src/dsp/cpu.h +@@ -47,12 +47,12 @@ + // x86 defines. + + #if !defined(HAVE_CONFIG_H) +-#if defined(_MSC_VER) && _MSC_VER > 1310 && \ ++#if defined(_MSC_VER) && !defined(__clang__) && _MSC_VER > 1310 && \ + (defined(_M_X64) || defined(_M_IX86)) + #define WEBP_MSC_SSE2 // Visual C++ SSE2 targets + #endif + +-#if defined(_MSC_VER) && _MSC_VER >= 1500 && \ ++#if defined(_MSC_VER) && !defined(__clang__) && _MSC_VER >= 1500 && \ + (defined(_M_X64) || defined(_M_IX86)) + #define WEBP_MSC_SSE41 // Visual C++ SSE4.1 targets + #endif diff --git a/thirdparty/libwebp/src/dsp/cpu.h b/thirdparty/libwebp/src/dsp/cpu.h index c86540f280..4dbe607aec 100644 --- a/thirdparty/libwebp/src/dsp/cpu.h +++ b/thirdparty/libwebp/src/dsp/cpu.h @@ -47,12 +47,12 @@ // x86 defines. #if !defined(HAVE_CONFIG_H) -#if defined(_MSC_VER) && _MSC_VER > 1310 && \ +#if defined(_MSC_VER) && !defined(__clang__) && _MSC_VER > 1310 && \ (defined(_M_X64) || defined(_M_IX86)) #define WEBP_MSC_SSE2 // Visual C++ SSE2 targets #endif -#if defined(_MSC_VER) && _MSC_VER >= 1500 && \ +#if defined(_MSC_VER) && !defined(__clang__) && _MSC_VER >= 1500 && \ (defined(_M_X64) || defined(_M_IX86)) #define WEBP_MSC_SSE41 // Visual C++ SSE4.1 targets #endif diff --git a/thirdparty/mbedtls/include/mbedtls/bignum.h b/thirdparty/mbedtls/include/mbedtls/bignum.h index 71d7b97672..8367cd34e6 100644 --- a/thirdparty/mbedtls/include/mbedtls/bignum.h +++ b/thirdparty/mbedtls/include/mbedtls/bignum.h @@ -880,7 +880,7 @@ int mbedtls_mpi_mod_int(mbedtls_mpi_uint *r, const mbedtls_mpi *A, mbedtls_mpi_sint b); /** - * \brief Perform a sliding-window exponentiation: X = A^E mod N + * \brief Perform a modular exponentiation: X = A^E mod N * * \param X The destination MPI. This must point to an initialized MPI. * This must not alias E or N. diff --git a/thirdparty/mbedtls/include/mbedtls/build_info.h b/thirdparty/mbedtls/include/mbedtls/build_info.h index eab167f383..8242ec6828 100644 --- a/thirdparty/mbedtls/include/mbedtls/build_info.h +++ b/thirdparty/mbedtls/include/mbedtls/build_info.h @@ -26,16 +26,16 @@ */ #define MBEDTLS_VERSION_MAJOR 3 #define MBEDTLS_VERSION_MINOR 6 -#define MBEDTLS_VERSION_PATCH 0 +#define MBEDTLS_VERSION_PATCH 1 /** * The single version number has the following structure: * MMNNPP00 * Major version | Minor version | Patch version */ -#define MBEDTLS_VERSION_NUMBER 0x03060000 -#define MBEDTLS_VERSION_STRING "3.6.0" -#define MBEDTLS_VERSION_STRING_FULL "Mbed TLS 3.6.0" +#define MBEDTLS_VERSION_NUMBER 0x03060100 +#define MBEDTLS_VERSION_STRING "3.6.1" +#define MBEDTLS_VERSION_STRING_FULL "Mbed TLS 3.6.1" /* Macros for build-time platform detection */ @@ -101,6 +101,13 @@ #define inline __inline #endif +#if defined(MBEDTLS_CONFIG_FILES_READ) +#error "Something went wrong: MBEDTLS_CONFIG_FILES_READ defined before reading the config files!" +#endif +#if defined(MBEDTLS_CONFIG_IS_FINALIZED) +#error "Something went wrong: MBEDTLS_CONFIG_IS_FINALIZED defined before reading the config files!" +#endif + /* X.509, TLS and non-PSA crypto configuration */ #if !defined(MBEDTLS_CONFIG_FILE) #include "mbedtls/mbedtls_config.h" @@ -135,6 +142,12 @@ #endif #endif /* defined(MBEDTLS_PSA_CRYPTO_CONFIG) */ +/* Indicate that all configuration files have been read. + * It is now time to adjust the configuration (follow through on dependencies, + * make PSA and legacy crypto consistent, etc.). + */ +#define MBEDTLS_CONFIG_FILES_READ + /* Auto-enable MBEDTLS_CTR_DRBG_USE_128_BIT_KEY if * MBEDTLS_AES_ONLY_128_BIT_KEY_LENGTH and MBEDTLS_CTR_DRBG_C defined * to ensure a 128-bit key size in CTR_DRBG. @@ -169,8 +182,13 @@ #include "mbedtls/config_adjust_ssl.h" -/* Make sure all configuration symbols are set before including check_config.h, - * even the ones that are calculated programmatically. */ +/* Indicate that all configuration symbols are set, + * even the ones that are calculated programmatically. + * It is now safe to query the configuration (to check it, to size buffers, + * etc.). + */ +#define MBEDTLS_CONFIG_IS_FINALIZED + #include "mbedtls/check_config.h" #endif /* MBEDTLS_BUILD_INFO_H */ diff --git a/thirdparty/mbedtls/include/mbedtls/check_config.h b/thirdparty/mbedtls/include/mbedtls/check_config.h index b3c038dd2e..67a05f83b8 100644 --- a/thirdparty/mbedtls/include/mbedtls/check_config.h +++ b/thirdparty/mbedtls/include/mbedtls/check_config.h @@ -2,6 +2,13 @@ * \file check_config.h * * \brief Consistency checks for configuration options + * + * This is an internal header. Do not include it directly. + * + * This header is included automatically by all public Mbed TLS headers + * (via mbedtls/build_info.h). Do not include it directly in a configuration + * file such as mbedtls/mbedtls_config.h or #MBEDTLS_USER_CONFIG_FILE! + * It would run at the wrong time due to missing derived symbols. */ /* * Copyright The Mbed TLS Contributors @@ -12,6 +19,13 @@ #define MBEDTLS_CHECK_CONFIG_H /* *INDENT-OFF* */ + +#if !defined(MBEDTLS_CONFIG_IS_FINALIZED) +#warning "Do not include mbedtls/check_config.h manually! " \ + "This may cause spurious errors. " \ + "It is included automatically at the right point since Mbed TLS 3.0." +#endif /* !MBEDTLS_CONFIG_IS_FINALIZED */ + /* * We assume CHAR_BIT is 8 in many places. In practice, this is true on our * target platforms, so not an issue, but let's just be extra sure. diff --git a/thirdparty/mbedtls/include/mbedtls/config_adjust_legacy_crypto.h b/thirdparty/mbedtls/include/mbedtls/config_adjust_legacy_crypto.h index 9b06041228..3ba987ebb2 100644 --- a/thirdparty/mbedtls/include/mbedtls/config_adjust_legacy_crypto.h +++ b/thirdparty/mbedtls/include/mbedtls/config_adjust_legacy_crypto.h @@ -2,7 +2,9 @@ * \file mbedtls/config_adjust_legacy_crypto.h * \brief Adjust legacy configuration configuration * - * Automatically enable certain dependencies. Generally, MBEDLTS_xxx + * This is an internal header. Do not include it directly. + * + * Automatically enable certain dependencies. Generally, MBEDTLS_xxx * configurations need to be explicitly enabled by the user: enabling * MBEDTLS_xxx_A but not MBEDTLS_xxx_B when A requires B results in a * compilation error. However, we do automatically enable certain options @@ -22,6 +24,14 @@ #ifndef MBEDTLS_CONFIG_ADJUST_LEGACY_CRYPTO_H #define MBEDTLS_CONFIG_ADJUST_LEGACY_CRYPTO_H +#if !defined(MBEDTLS_CONFIG_FILES_READ) +#error "Do not include mbedtls/config_adjust_*.h manually! This can lead to problems, " \ + "up to and including runtime errors such as buffer overflows. " \ + "If you're trying to fix a complaint from check_config.h, just remove " \ + "it from your configuration file: since Mbed TLS 3.0, it is included " \ + "automatically at the right point." +#endif /* */ + /* Ideally, we'd set those as defaults in mbedtls_config.h, but * putting an #ifdef _WIN32 in mbedtls_config.h would confuse config.py. * @@ -48,7 +58,8 @@ defined(MBEDTLS_PSA_BUILTIN_ALG_ECB_NO_PADDING) || \ defined(MBEDTLS_PSA_BUILTIN_ALG_CBC_NO_PADDING) || \ defined(MBEDTLS_PSA_BUILTIN_ALG_CBC_PKCS7) || \ - defined(MBEDTLS_PSA_BUILTIN_ALG_CCM_STAR_NO_TAG)) + defined(MBEDTLS_PSA_BUILTIN_ALG_CCM_STAR_NO_TAG) || \ + defined(MBEDTLS_PSA_BUILTIN_ALG_CMAC)) #define MBEDTLS_CIPHER_C #endif @@ -293,6 +304,14 @@ #define MBEDTLS_ECP_LIGHT #endif +/* Backward compatibility: after #8740 the RSA module offers functions to parse + * and write RSA private/public keys without relying on the PK one. Of course + * this needs ASN1 support to do so, so we enable it here. */ +#if defined(MBEDTLS_RSA_C) +#define MBEDTLS_ASN1_PARSE_C +#define MBEDTLS_ASN1_WRITE_C +#endif + /* MBEDTLS_PK_PARSE_EC_COMPRESSED is introduced in Mbed TLS version 3.5, while * in previous version compressed points were automatically supported as long * as PK_PARSE_C and ECP_C were enabled. As a consequence, for backward @@ -409,12 +428,12 @@ /* psa_util file features some ECDSA conversion functions, to convert between * legacy's ASN.1 DER format and PSA's raw one. */ -#if defined(MBEDTLS_ECDSA_C) || (defined(MBEDTLS_PSA_CRYPTO_C) && \ +#if (defined(MBEDTLS_PSA_CRYPTO_CLIENT) && \ (defined(PSA_WANT_ALG_ECDSA) || defined(PSA_WANT_ALG_DETERMINISTIC_ECDSA))) #define MBEDTLS_PSA_UTIL_HAVE_ECDSA #endif -/* Some internal helpers to determine which keys are availble. */ +/* Some internal helpers to determine which keys are available. */ #if (!defined(MBEDTLS_USE_PSA_CRYPTO) && defined(MBEDTLS_AES_C)) || \ (defined(MBEDTLS_USE_PSA_CRYPTO) && defined(PSA_WANT_KEY_TYPE_AES)) #define MBEDTLS_SSL_HAVE_AES @@ -428,7 +447,7 @@ #define MBEDTLS_SSL_HAVE_CAMELLIA #endif -/* Some internal helpers to determine which operation modes are availble. */ +/* Some internal helpers to determine which operation modes are available. */ #if (!defined(MBEDTLS_USE_PSA_CRYPTO) && defined(MBEDTLS_CIPHER_MODE_CBC)) || \ (defined(MBEDTLS_USE_PSA_CRYPTO) && defined(PSA_WANT_ALG_CBC_NO_PADDING)) #define MBEDTLS_SSL_HAVE_CBC diff --git a/thirdparty/mbedtls/include/mbedtls/config_adjust_legacy_from_psa.h b/thirdparty/mbedtls/include/mbedtls/config_adjust_legacy_from_psa.h index 0091e246b2..04bdae61bb 100644 --- a/thirdparty/mbedtls/include/mbedtls/config_adjust_legacy_from_psa.h +++ b/thirdparty/mbedtls/include/mbedtls/config_adjust_legacy_from_psa.h @@ -2,6 +2,8 @@ * \file mbedtls/config_adjust_legacy_from_psa.h * \brief Adjust PSA configuration: activate legacy implementations * + * This is an internal header. Do not include it directly. + * * When MBEDTLS_PSA_CRYPTO_CONFIG is enabled, activate legacy implementations * of cryptographic mechanisms as needed to fulfill the needs of the PSA * configuration. Generally speaking, we activate a legacy mechanism if @@ -16,6 +18,14 @@ #ifndef MBEDTLS_CONFIG_ADJUST_LEGACY_FROM_PSA_H #define MBEDTLS_CONFIG_ADJUST_LEGACY_FROM_PSA_H +#if !defined(MBEDTLS_CONFIG_FILES_READ) +#error "Do not include mbedtls/config_adjust_*.h manually! This can lead to problems, " \ + "up to and including runtime errors such as buffer overflows. " \ + "If you're trying to fix a complaint from check_config.h, just remove " \ + "it from your configuration file: since Mbed TLS 3.0, it is included " \ + "automatically at the right point." +#endif /* */ + /* Define appropriate ACCEL macros for the p256-m driver. * In the future, those should be generated from the drivers JSON description. */ @@ -498,7 +508,6 @@ * The PSA implementation has its own implementation of HKDF, separate from * hkdf.c. No need to enable MBEDTLS_HKDF_C here. */ -#define MBEDTLS_PSA_BUILTIN_ALG_HMAC 1 #define MBEDTLS_PSA_BUILTIN_ALG_HKDF 1 #endif /* !MBEDTLS_PSA_ACCEL_ALG_HKDF */ #endif /* PSA_WANT_ALG_HKDF */ @@ -509,7 +518,6 @@ * The PSA implementation has its own implementation of HKDF, separate from * hkdf.c. No need to enable MBEDTLS_HKDF_C here. */ -#define MBEDTLS_PSA_BUILTIN_ALG_HMAC 1 #define MBEDTLS_PSA_BUILTIN_ALG_HKDF_EXTRACT 1 #endif /* !MBEDTLS_PSA_ACCEL_ALG_HKDF_EXTRACT */ #endif /* PSA_WANT_ALG_HKDF_EXTRACT */ @@ -520,7 +528,6 @@ * The PSA implementation has its own implementation of HKDF, separate from * hkdf.c. No need to enable MBEDTLS_HKDF_C here. */ -#define MBEDTLS_PSA_BUILTIN_ALG_HMAC 1 #define MBEDTLS_PSA_BUILTIN_ALG_HKDF_EXPAND 1 #endif /* !MBEDTLS_PSA_ACCEL_ALG_HKDF_EXPAND */ #endif /* PSA_WANT_ALG_HKDF_EXPAND */ @@ -630,9 +637,6 @@ #if !defined(MBEDTLS_PSA_ACCEL_ALG_PBKDF2_HMAC) #define MBEDTLS_PSA_BUILTIN_ALG_PBKDF2_HMAC 1 #define PSA_HAVE_SOFT_PBKDF2_HMAC 1 -#if !defined(MBEDTLS_PSA_ACCEL_ALG_HMAC) -#define MBEDTLS_PSA_BUILTIN_ALG_HMAC 1 -#endif /* !MBEDTLS_PSA_ACCEL_ALG_HMAC */ #endif /* !MBEDTLS_PSA_BUILTIN_ALG_PBKDF2_HMAC */ #endif /* PSA_WANT_ALG_PBKDF2_HMAC */ diff --git a/thirdparty/mbedtls/include/mbedtls/config_adjust_psa_from_legacy.h b/thirdparty/mbedtls/include/mbedtls/config_adjust_psa_from_legacy.h index 3456615943..14ca14696f 100644 --- a/thirdparty/mbedtls/include/mbedtls/config_adjust_psa_from_legacy.h +++ b/thirdparty/mbedtls/include/mbedtls/config_adjust_psa_from_legacy.h @@ -2,6 +2,8 @@ * \file mbedtls/config_adjust_psa_from_legacy.h * \brief Adjust PSA configuration: construct PSA configuration from legacy * + * This is an internal header. Do not include it directly. + * * When MBEDTLS_PSA_CRYPTO_CONFIG is disabled, we automatically enable * cryptographic mechanisms through the PSA interface when the corresponding * legacy mechanism is enabled. In many cases, this just enables the PSA @@ -18,6 +20,14 @@ #ifndef MBEDTLS_CONFIG_ADJUST_PSA_FROM_LEGACY_H #define MBEDTLS_CONFIG_ADJUST_PSA_FROM_LEGACY_H +#if !defined(MBEDTLS_CONFIG_FILES_READ) +#error "Do not include mbedtls/config_adjust_*.h manually! This can lead to problems, " \ + "up to and including runtime errors such as buffer overflows. " \ + "If you're trying to fix a complaint from check_config.h, just remove " \ + "it from your configuration file: since Mbed TLS 3.0, it is included " \ + "automatically at the right point." +#endif /* */ + /* * Ensure PSA_WANT_* defines are setup properly if MBEDTLS_PSA_CRYPTO_CONFIG * is not defined diff --git a/thirdparty/mbedtls/include/mbedtls/config_adjust_psa_superset_legacy.h b/thirdparty/mbedtls/include/mbedtls/config_adjust_psa_superset_legacy.h index 3a55c3f6e1..ef65cce0d9 100644 --- a/thirdparty/mbedtls/include/mbedtls/config_adjust_psa_superset_legacy.h +++ b/thirdparty/mbedtls/include/mbedtls/config_adjust_psa_superset_legacy.h @@ -2,6 +2,8 @@ * \file mbedtls/config_adjust_psa_superset_legacy.h * \brief Adjust PSA configuration: automatic enablement from legacy * + * This is an internal header. Do not include it directly. + * * To simplify some edge cases, we automatically enable certain cryptographic * mechanisms in the PSA API if they are enabled in the legacy API. The general * idea is that if legacy module M uses mechanism A internally, and A has @@ -17,6 +19,14 @@ #ifndef MBEDTLS_CONFIG_ADJUST_PSA_SUPERSET_LEGACY_H #define MBEDTLS_CONFIG_ADJUST_PSA_SUPERSET_LEGACY_H +#if !defined(MBEDTLS_CONFIG_FILES_READ) +#error "Do not include mbedtls/config_adjust_*.h manually! This can lead to problems, " \ + "up to and including runtime errors such as buffer overflows. " \ + "If you're trying to fix a complaint from check_config.h, just remove " \ + "it from your configuration file: since Mbed TLS 3.0, it is included " \ + "automatically at the right point." +#endif /* */ + /****************************************************************/ /* Hashes that are built in are also enabled in PSA. * This simplifies dependency declarations especially diff --git a/thirdparty/mbedtls/include/mbedtls/config_adjust_ssl.h b/thirdparty/mbedtls/include/mbedtls/config_adjust_ssl.h index 39c7b3b117..1f82d9c006 100644 --- a/thirdparty/mbedtls/include/mbedtls/config_adjust_ssl.h +++ b/thirdparty/mbedtls/include/mbedtls/config_adjust_ssl.h @@ -2,7 +2,9 @@ * \file mbedtls/config_adjust_ssl.h * \brief Adjust TLS configuration * - * Automatically enable certain dependencies. Generally, MBEDLTS_xxx + * This is an internal header. Do not include it directly. + * + * Automatically enable certain dependencies. Generally, MBEDTLS_xxx * configurations need to be explicitly enabled by the user: enabling * MBEDTLS_xxx_A but not MBEDTLS_xxx_B when A requires B results in a * compilation error. However, we do automatically enable certain options @@ -22,6 +24,14 @@ #ifndef MBEDTLS_CONFIG_ADJUST_SSL_H #define MBEDTLS_CONFIG_ADJUST_SSL_H +#if !defined(MBEDTLS_CONFIG_FILES_READ) +#error "Do not include mbedtls/config_adjust_*.h manually! This can lead to problems, " \ + "up to and including runtime errors such as buffer overflows. " \ + "If you're trying to fix a complaint from check_config.h, just remove " \ + "it from your configuration file: since Mbed TLS 3.0, it is included " \ + "automatically at the right point." +#endif /* */ + /* The following blocks make it easier to disable all of TLS, * or of TLS 1.2 or 1.3 or DTLS, without having to manually disable all * key exchanges, options and extensions related to them. */ diff --git a/thirdparty/mbedtls/include/mbedtls/config_adjust_x509.h b/thirdparty/mbedtls/include/mbedtls/config_adjust_x509.h index 346c8ae6d5..cfb2d88916 100644 --- a/thirdparty/mbedtls/include/mbedtls/config_adjust_x509.h +++ b/thirdparty/mbedtls/include/mbedtls/config_adjust_x509.h @@ -2,7 +2,9 @@ * \file mbedtls/config_adjust_x509.h * \brief Adjust X.509 configuration * - * Automatically enable certain dependencies. Generally, MBEDLTS_xxx + * This is an internal header. Do not include it directly. + * + * Automatically enable certain dependencies. Generally, MBEDTLS_xxx * configurations need to be explicitly enabled by the user: enabling * MBEDTLS_xxx_A but not MBEDTLS_xxx_B when A requires B results in a * compilation error. However, we do automatically enable certain options @@ -22,4 +24,12 @@ #ifndef MBEDTLS_CONFIG_ADJUST_X509_H #define MBEDTLS_CONFIG_ADJUST_X509_H +#if !defined(MBEDTLS_CONFIG_FILES_READ) +#error "Do not include mbedtls/config_adjust_*.h manually! This can lead to problems, " \ + "up to and including runtime errors such as buffer overflows. " \ + "If you're trying to fix a complaint from check_config.h, just remove " \ + "it from your configuration file: since Mbed TLS 3.0, it is included " \ + "automatically at the right point." +#endif /* */ + #endif /* MBEDTLS_CONFIG_ADJUST_X509_H */ diff --git a/thirdparty/mbedtls/include/mbedtls/config_psa.h b/thirdparty/mbedtls/include/mbedtls/config_psa.h index 17da61b3e8..5f3d0f3d5d 100644 --- a/thirdparty/mbedtls/include/mbedtls/config_psa.h +++ b/thirdparty/mbedtls/include/mbedtls/config_psa.h @@ -22,6 +22,8 @@ #include "psa/crypto_adjust_config_synonyms.h" +#include "psa/crypto_adjust_config_dependencies.h" + #include "mbedtls/config_adjust_psa_superset_legacy.h" #if defined(MBEDTLS_PSA_CRYPTO_CONFIG) @@ -32,7 +34,11 @@ * before we deduce what built-ins are required. */ #include "psa/crypto_adjust_config_key_pair_types.h" +#if defined(MBEDTLS_PSA_CRYPTO_C) +/* If we are implementing PSA crypto ourselves, then we want to enable the + * required built-ins. Otherwise, PSA features will be provided by the server. */ #include "mbedtls/config_adjust_legacy_from_psa.h" +#endif #else /* MBEDTLS_PSA_CRYPTO_CONFIG */ diff --git a/thirdparty/mbedtls/include/mbedtls/ctr_drbg.h b/thirdparty/mbedtls/include/mbedtls/ctr_drbg.h index c00756df1b..0b7cce1923 100644 --- a/thirdparty/mbedtls/include/mbedtls/ctr_drbg.h +++ b/thirdparty/mbedtls/include/mbedtls/ctr_drbg.h @@ -32,12 +32,27 @@ #include "mbedtls/build_info.h" -/* In case AES_C is defined then it is the primary option for backward - * compatibility purposes. If that's not available, PSA is used instead */ -#if defined(MBEDTLS_AES_C) -#include "mbedtls/aes.h" -#else +/* The CTR_DRBG implementation can either directly call the low-level AES + * module (gated by MBEDTLS_AES_C) or call the PSA API to perform AES + * operations. Calling the AES module directly is the default, both for + * maximum backward compatibility and because it's a bit more efficient + * (less glue code). + * + * When MBEDTLS_AES_C is disabled, the CTR_DRBG module calls PSA crypto and + * thus benefits from the PSA AES accelerator driver. + * It is technically possible to enable MBEDTLS_CTR_DRBG_USE_PSA_CRYPTO + * to use PSA even when MBEDTLS_AES_C is enabled, but there is very little + * reason to do so other than testing purposes and this is not officially + * supported. + */ +#if !defined(MBEDTLS_AES_C) +#define MBEDTLS_CTR_DRBG_USE_PSA_CRYPTO +#endif + +#if defined(MBEDTLS_CTR_DRBG_USE_PSA_CRYPTO) #include "psa/crypto.h" +#else +#include "mbedtls/aes.h" #endif #include "entropy.h" @@ -157,7 +172,7 @@ extern "C" { #define MBEDTLS_CTR_DRBG_ENTROPY_NONCE_LEN (MBEDTLS_CTR_DRBG_ENTROPY_LEN + 1) / 2 #endif -#if !defined(MBEDTLS_AES_C) +#if defined(MBEDTLS_CTR_DRBG_USE_PSA_CRYPTO) typedef struct mbedtls_ctr_drbg_psa_context { mbedtls_svc_key_id_t key_id; psa_cipher_operation_t operation; @@ -189,10 +204,10 @@ typedef struct mbedtls_ctr_drbg_context { * This is the maximum number of requests * that can be made between reseedings. */ -#if defined(MBEDTLS_AES_C) - mbedtls_aes_context MBEDTLS_PRIVATE(aes_ctx); /*!< The AES context. */ -#else +#if defined(MBEDTLS_CTR_DRBG_USE_PSA_CRYPTO) mbedtls_ctr_drbg_psa_context MBEDTLS_PRIVATE(psa_ctx); /*!< The PSA context. */ +#else + mbedtls_aes_context MBEDTLS_PRIVATE(aes_ctx); /*!< The AES context. */ #endif /* diff --git a/thirdparty/mbedtls/include/mbedtls/ecdh.h b/thirdparty/mbedtls/include/mbedtls/ecdh.h index a0909d6b44..a6a5069337 100644 --- a/thirdparty/mbedtls/include/mbedtls/ecdh.h +++ b/thirdparty/mbedtls/include/mbedtls/ecdh.h @@ -325,7 +325,7 @@ int mbedtls_ecdh_read_params(mbedtls_ecdh_context *ctx, * \brief This function sets up an ECDH context from an EC key. * * It is used by clients and servers in place of the - * ServerKeyEchange for static ECDH, and imports ECDH + * ServerKeyExchange for static ECDH, and imports ECDH * parameters from the EC key information of a certificate. * * \see ecp.h diff --git a/thirdparty/mbedtls/include/mbedtls/ecp.h b/thirdparty/mbedtls/include/mbedtls/ecp.h index d8f73ae965..623910bcbd 100644 --- a/thirdparty/mbedtls/include/mbedtls/ecp.h +++ b/thirdparty/mbedtls/include/mbedtls/ecp.h @@ -216,7 +216,7 @@ mbedtls_ecp_point; * range of <code>0..2^(2*pbits)-1</code>, and transforms it in-place to an integer * which is congruent mod \p P to the given MPI, and is close enough to \p pbits * in size, so that it may be efficiently brought in the 0..P-1 range by a few - * additions or subtractions. Therefore, it is only an approximative modular + * additions or subtractions. Therefore, it is only an approximate modular * reduction. It must return 0 on success and non-zero on failure. * * \note Alternative implementations of the ECP module must obey the diff --git a/thirdparty/mbedtls/include/mbedtls/mbedtls_config.h b/thirdparty/mbedtls/include/mbedtls/mbedtls_config.h index 35921412c6..bd3f71d5bc 100644 --- a/thirdparty/mbedtls/include/mbedtls/mbedtls_config.h +++ b/thirdparty/mbedtls/include/mbedtls/mbedtls_config.h @@ -1118,7 +1118,7 @@ * MBEDTLS_ECP_DP_SECP256R1_ENABLED * * \warning If SHA-256 is provided only by a PSA driver, you must call - * psa_crypto_init() before the first hanshake (even if + * psa_crypto_init() before the first handshake (even if * MBEDTLS_USE_PSA_CRYPTO is disabled). * * This enables the following ciphersuites (if other requisites are @@ -1415,6 +1415,23 @@ //#define MBEDTLS_PSA_CRYPTO_SPM /** + * \def MBEDTLS_PSA_KEY_STORE_DYNAMIC + * + * Dynamically resize the PSA key store to accommodate any number of + * volatile keys (until the heap memory is exhausted). + * + * If this option is disabled, the key store has a fixed size + * #MBEDTLS_PSA_KEY_SLOT_COUNT for volatile keys and loaded persistent keys + * together. + * + * This option has no effect when #MBEDTLS_PSA_CRYPTO_C is disabled. + * + * Module: library/psa_crypto.c + * Requires: MBEDTLS_PSA_CRYPTO_C + */ +#define MBEDTLS_PSA_KEY_STORE_DYNAMIC + +/** * Uncomment to enable p256-m. This is an alternative implementation of * key generation, ECDH and (randomized) ECDSA on the curve SECP256R1. * Compared to the default implementation: @@ -1781,8 +1798,9 @@ * Requires: MBEDTLS_PSA_CRYPTO_C * * \note TLS 1.3 uses PSA crypto for cryptographic operations that are - * directly performed by TLS 1.3 code. As a consequence, you must - * call psa_crypto_init() before the first TLS 1.3 handshake. + * directly performed by TLS 1.3 code. As a consequence, when TLS 1.3 + * is enabled, a TLS handshake may call psa_crypto_init(), even + * if it ends up negotiating a different TLS version. * * \note Cryptographic operations performed indirectly via another module * (X.509, PK) or by code shared with TLS 1.2 (record protection, @@ -2625,7 +2643,7 @@ * The CTR_DRBG generator uses AES-256 by default. * To use AES-128 instead, enable \c MBEDTLS_CTR_DRBG_USE_128_BIT_KEY above. * - * AES support can either be achived through builtin (MBEDTLS_AES_C) or PSA. + * AES support can either be achieved through builtin (MBEDTLS_AES_C) or PSA. * Builtin is the default option when MBEDTLS_AES_C is defined otherwise PSA * is used. * @@ -4016,22 +4034,38 @@ * Use HMAC_DRBG with the specified hash algorithm for HMAC_DRBG for the * PSA crypto subsystem. * - * If this option is unset: - * - If CTR_DRBG is available, the PSA subsystem uses it rather than HMAC_DRBG. - * - Otherwise, the PSA subsystem uses HMAC_DRBG with either - * #MBEDTLS_MD_SHA512 or #MBEDTLS_MD_SHA256 based on availability and - * on unspecified heuristics. + * If this option is unset, the library chooses a hash (currently between + * #MBEDTLS_MD_SHA512 and #MBEDTLS_MD_SHA256) based on availability and + * unspecified heuristics. + * + * \note The PSA crypto subsystem uses the first available mechanism amongst + * the following: + * - #MBEDTLS_PSA_CRYPTO_EXTERNAL_RNG if enabled; + * - Entropy from #MBEDTLS_ENTROPY_C plus CTR_DRBG with AES + * if #MBEDTLS_CTR_DRBG_C is enabled; + * - Entropy from #MBEDTLS_ENTROPY_C plus HMAC_DRBG. + * + * A future version may reevaluate the prioritization of DRBG mechanisms. */ //#define MBEDTLS_PSA_HMAC_DRBG_MD_TYPE MBEDTLS_MD_SHA256 /** \def MBEDTLS_PSA_KEY_SLOT_COUNT - * Restrict the PSA library to supporting a maximum amount of simultaneously - * loaded keys. A loaded key is a key stored by the PSA Crypto core as a - * volatile key, or a persistent key which is loaded temporarily by the - * library as part of a crypto operation in flight. * - * If this option is unset, the library will fall back to a default value of - * 32 keys. + * When #MBEDTLS_PSA_KEY_STORE_DYNAMIC is disabled, + * the maximum amount of PSA keys simultaneously in memory. This counts all + * volatile keys, plus loaded persistent keys. + * + * When #MBEDTLS_PSA_KEY_STORE_DYNAMIC is enabled, + * the maximum number of loaded persistent keys. + * + * Currently, persistent keys do not need to be loaded all the time while + * a multipart operation is in progress, only while the operation is being + * set up. This may change in future versions of the library. + * + * Currently, the library traverses of the whole table on each access to a + * persistent key. Therefore large values may cause poor performance. + * + * This option has no effect when #MBEDTLS_PSA_CRYPTO_C is disabled. */ //#define MBEDTLS_PSA_KEY_SLOT_COUNT 32 diff --git a/thirdparty/mbedtls/include/mbedtls/pk.h b/thirdparty/mbedtls/include/mbedtls/pk.h index fde302f872..52f4cc6c9e 100644 --- a/thirdparty/mbedtls/include/mbedtls/pk.h +++ b/thirdparty/mbedtls/include/mbedtls/pk.h @@ -359,32 +359,40 @@ int mbedtls_pk_setup(mbedtls_pk_context *ctx, const mbedtls_pk_info_t *info); #if defined(MBEDTLS_USE_PSA_CRYPTO) /** - * \brief Initialize a PK context to wrap a PSA key. - * - * \note This function replaces mbedtls_pk_setup() for contexts - * that wrap a (possibly opaque) PSA key instead of - * storing and manipulating the key material directly. - * - * \param ctx The context to initialize. It must be empty (type NONE). - * \param key The PSA key to wrap, which must hold an ECC or RSA key - * pair (see notes below). - * - * \note The wrapped key must remain valid as long as the - * wrapping PK context is in use, that is at least between - * the point this function is called and the point - * mbedtls_pk_free() is called on this context. The wrapped - * key might then be independently used or destroyed. - * - * \note This function is currently only available for ECC or RSA - * key pairs (that is, keys containing private key material). - * Support for other key types may be added later. - * - * \return \c 0 on success. - * \return #MBEDTLS_ERR_PK_BAD_INPUT_DATA on invalid input - * (context already used, invalid key identifier). - * \return #MBEDTLS_ERR_PK_FEATURE_UNAVAILABLE if the key is not an - * ECC key pair. - * \return #MBEDTLS_ERR_PK_ALLOC_FAILED on allocation failure. + * \brief Initialize a PK context to wrap a PSA key. + * + * This function creates a PK context which wraps a PSA key. The PSA wrapped + * key must be an EC or RSA key pair (DH is not suported in the PK module). + * + * Under the hood PSA functions will be used to perform the required + * operations and, based on the key type, used algorithms will be: + * * EC: + * * verify, verify_ext, sign, sign_ext: ECDSA. + * * RSA: + * * sign, decrypt: use the primary algorithm in the wrapped PSA key; + * * sign_ext: RSA PSS if the pk_type is #MBEDTLS_PK_RSASSA_PSS, otherwise + * it falls back to the sign() case; + * * verify, verify_ext, encrypt: not supported. + * + * In order for the above operations to succeed, the policy of the wrapped PSA + * key must allow the specified algorithm. + * + * Opaque PK contexts wrapping an EC keys also support \c mbedtls_pk_check_pair(), + * whereas RSA ones do not. + * + * \warning The PSA wrapped key must remain valid as long as the wrapping PK + * context is in use, that is at least between the point this function + * is called and the point mbedtls_pk_free() is called on this context. + * + * \param ctx The context to initialize. It must be empty (type NONE). + * \param key The PSA key to wrap, which must hold an ECC or RSA key pair. + * + * \return \c 0 on success. + * \return #MBEDTLS_ERR_PK_BAD_INPUT_DATA on invalid input (context already + * used, invalid key identifier). + * \return #MBEDTLS_ERR_PK_FEATURE_UNAVAILABLE if the key is not an ECC or + * RSA key pair. + * \return #MBEDTLS_ERR_PK_ALLOC_FAILED on allocation failure. */ int mbedtls_pk_setup_opaque(mbedtls_pk_context *ctx, const mbedtls_svc_key_id_t key); diff --git a/thirdparty/mbedtls/include/mbedtls/ssl.h b/thirdparty/mbedtls/include/mbedtls/ssl.h index 172d4693b2..42fffbf860 100644 --- a/thirdparty/mbedtls/include/mbedtls/ssl.h +++ b/thirdparty/mbedtls/include/mbedtls/ssl.h @@ -83,10 +83,7 @@ /** Processing of the Certificate handshake message failed. */ #define MBEDTLS_ERR_SSL_BAD_CERTIFICATE -0x7A00 /* Error space gap */ -/** - * Received NewSessionTicket Post Handshake Message. - * This error code is experimental and may be changed or removed without notice. - */ +/** A TLS 1.3 NewSessionTicket message has been received. */ #define MBEDTLS_ERR_SSL_RECEIVED_NEW_SESSION_TICKET -0x7B00 /** Not possible to read early data */ #define MBEDTLS_ERR_SSL_CANNOT_READ_EARLY_DATA -0x7B80 @@ -324,6 +321,9 @@ #define MBEDTLS_SSL_SESSION_TICKETS_DISABLED 0 #define MBEDTLS_SSL_SESSION_TICKETS_ENABLED 1 +#define MBEDTLS_SSL_TLS1_3_SIGNAL_NEW_SESSION_TICKETS_DISABLED 0 +#define MBEDTLS_SSL_TLS1_3_SIGNAL_NEW_SESSION_TICKETS_ENABLED 1 + #define MBEDTLS_SSL_PRESET_DEFAULT 0 #define MBEDTLS_SSL_PRESET_SUITEB 2 @@ -1446,6 +1446,12 @@ struct mbedtls_ssl_config { #endif #if defined(MBEDTLS_SSL_SESSION_TICKETS) && \ defined(MBEDTLS_SSL_CLI_C) + /** Encodes two booleans, one stating whether TLS 1.2 session tickets are + * enabled or not, the other one whether the handling of TLS 1.3 + * NewSessionTicket messages is enabled or not. They are respectively set + * by mbedtls_ssl_conf_session_tickets() and + * mbedtls_ssl_conf_tls13_enable_signal_new_session_tickets(). + */ uint8_t MBEDTLS_PRIVATE(session_tickets); /*!< use session tickets? */ #endif @@ -2364,7 +2370,7 @@ int mbedtls_ssl_set_cid(mbedtls_ssl_context *ssl, */ int mbedtls_ssl_get_own_cid(mbedtls_ssl_context *ssl, int *enabled, - unsigned char own_cid[MBEDTLS_SSL_CID_OUT_LEN_MAX], + unsigned char own_cid[MBEDTLS_SSL_CID_IN_LEN_MAX], size_t *own_cid_len); /** @@ -3216,16 +3222,16 @@ void mbedtls_ssl_conf_session_cache(mbedtls_ssl_config *conf, * a full handshake. * * \note This function can handle a variety of mechanisms for session - * resumption: For TLS 1.2, both session ID-based resumption and - * ticket-based resumption will be considered. For TLS 1.3, - * once implemented, sessions equate to tickets, and loading - * one or more sessions via this call will lead to their - * corresponding tickets being advertised as resumption PSKs - * by the client. - * - * \note Calling this function multiple times will only be useful - * once TLS 1.3 is supported. For TLS 1.2 connections, this - * function should be called at most once. + * resumption: For TLS 1.2, both session ID-based resumption + * and ticket-based resumption will be considered. For TLS 1.3, + * sessions equate to tickets, and loading one session by + * calling this function will lead to its corresponding ticket + * being advertised as resumption PSK by the client. This + * depends on session tickets being enabled (see + * #MBEDTLS_SSL_SESSION_TICKETS configuration option) though. + * If session tickets are disabled, a call to this function + * with a TLS 1.3 session, will not have any effect on the next + * handshake for the SSL context \p ssl. * * \param ssl The SSL context representing the connection which should * be attempted to be setup using session resumption. This @@ -3240,9 +3246,10 @@ void mbedtls_ssl_conf_session_cache(mbedtls_ssl_config *conf, * * \return \c 0 if successful. * \return \c MBEDTLS_ERR_SSL_FEATURE_UNAVAILABLE if the session - * could not be loaded because of an implementation limitation. - * This error is non-fatal, and has no observable effect on - * the SSL context or the session that was attempted to be loaded. + * could not be loaded because one session has already been + * loaded. This error is non-fatal, and has no observable + * effect on the SSL context or the session that was attempted + * to be loaded. * \return Another negative error code on other kinds of failure. * * \sa mbedtls_ssl_get_session() @@ -3309,8 +3316,16 @@ int mbedtls_ssl_session_load(mbedtls_ssl_session *session, * to determine the necessary size by calling this function * with \p buf set to \c NULL and \p buf_len to \c 0. * + * \note For TLS 1.3 sessions, this feature is supported only if the + * MBEDTLS_SSL_SESSION_TICKETS configuration option is enabled, + * as in TLS 1.3 session resumption is possible only with + * tickets. + * * \return \c 0 if successful. * \return #MBEDTLS_ERR_SSL_BUFFER_TOO_SMALL if \p buf is too small. + * \return #MBEDTLS_ERR_SSL_FEATURE_UNAVAILABLE if the + * MBEDTLS_SSL_SESSION_TICKETS configuration option is disabled + * and the session is a TLS 1.3 session. */ int mbedtls_ssl_session_save(const mbedtls_ssl_session *session, unsigned char *buf, @@ -4456,21 +4471,50 @@ int mbedtls_ssl_conf_max_frag_len(mbedtls_ssl_config *conf, unsigned char mfl_co void mbedtls_ssl_conf_preference_order(mbedtls_ssl_config *conf, int order); #endif /* MBEDTLS_SSL_SRV_C */ -#if defined(MBEDTLS_SSL_SESSION_TICKETS) && \ - defined(MBEDTLS_SSL_CLI_C) +#if defined(MBEDTLS_SSL_SESSION_TICKETS) && defined(MBEDTLS_SSL_CLI_C) /** - * \brief Enable / Disable session tickets (client only). - * (Default: MBEDTLS_SSL_SESSION_TICKETS_ENABLED.) + * \brief Enable / Disable TLS 1.2 session tickets (client only, + * TLS 1.2 only). Enabled by default. * * \note On server, use \c mbedtls_ssl_conf_session_tickets_cb(). * * \param conf SSL configuration - * \param use_tickets Enable or disable (MBEDTLS_SSL_SESSION_TICKETS_ENABLED or - * MBEDTLS_SSL_SESSION_TICKETS_DISABLED) + * \param use_tickets Enable or disable (#MBEDTLS_SSL_SESSION_TICKETS_ENABLED or + * #MBEDTLS_SSL_SESSION_TICKETS_DISABLED) */ void mbedtls_ssl_conf_session_tickets(mbedtls_ssl_config *conf, int use_tickets); -#endif /* MBEDTLS_SSL_SESSION_TICKETS && - MBEDTLS_SSL_CLI_C */ + +#if defined(MBEDTLS_SSL_PROTO_TLS1_3) +/** + * \brief Enable / Disable handling of TLS 1.3 NewSessionTicket messages + * (client only, TLS 1.3 only). + * + * The handling of TLS 1.3 NewSessionTicket messages is disabled by + * default. + * + * In TLS 1.3, servers may send a NewSessionTicket message at any time, + * and may send multiple NewSessionTicket messages. By default, TLS 1.3 + * clients ignore NewSessionTicket messages. + * + * To support session tickets in TLS 1.3 clients, call this function + * with #MBEDTLS_SSL_TLS1_3_SIGNAL_NEW_SESSION_TICKETS_ENABLED. When + * this is enabled, when a client receives a NewSessionTicket message, + * the next call to a message processing functions (notably + * mbedtls_ssl_handshake() and mbedtls_ssl_read()) will return + * #MBEDTLS_ERR_SSL_RECEIVED_NEW_SESSION_TICKET. The client should then + * call mbedtls_ssl_get_session() to retrieve the session ticket before + * calling the same message processing function again. + * + * \param conf SSL configuration + * \param signal_new_session_tickets Enable or disable + * (#MBEDTLS_SSL_TLS1_3_SIGNAL_NEW_SESSION_TICKETS_ENABLED or + * #MBEDTLS_SSL_TLS1_3_SIGNAL_NEW_SESSION_TICKETS_DISABLED) + */ +void mbedtls_ssl_conf_tls13_enable_signal_new_session_tickets( + mbedtls_ssl_config *conf, int signal_new_session_tickets); + +#endif /* MBEDTLS_SSL_PROTO_TLS1_3 */ +#endif /* MBEDTLS_SSL_SESSION_TICKETS && MBEDTLS_SSL_CLI_C */ #if defined(MBEDTLS_SSL_SESSION_TICKETS) && \ defined(MBEDTLS_SSL_SRV_C) && \ @@ -4837,23 +4881,16 @@ const mbedtls_x509_crt *mbedtls_ssl_get_peer_cert(const mbedtls_ssl_context *ssl * \note This function can handle a variety of mechanisms for session * resumption: For TLS 1.2, both session ID-based resumption and * ticket-based resumption will be considered. For TLS 1.3, - * once implemented, sessions equate to tickets, and calling - * this function multiple times will export the available - * tickets one a time until no further tickets are available, - * in which case MBEDTLS_ERR_SSL_FEATURE_UNAVAILABLE will - * be returned. - * - * \note Calling this function multiple times will only be useful - * once TLS 1.3 is supported. For TLS 1.2 connections, this - * function should be called at most once. + * sessions equate to tickets, and if session tickets are + * enabled (see #MBEDTLS_SSL_SESSION_TICKETS configuration + * option), this function exports the last received ticket and + * the exported session may be used to resume the TLS 1.3 + * session. If session tickets are disabled, exported sessions + * cannot be used to resume a TLS 1.3 session. * * \return \c 0 if successful. In this case, \p session can be used for * session resumption by passing it to mbedtls_ssl_set_session(), * and serialized for storage via mbedtls_ssl_session_save(). - * \return #MBEDTLS_ERR_SSL_FEATURE_UNAVAILABLE if no further session - * is available for export. - * This error is a non-fatal, and has no observable effect on - * the SSL context or the destination session. * \return Another negative error code on other kinds of failure. * * \sa mbedtls_ssl_set_session() @@ -4885,6 +4922,10 @@ int mbedtls_ssl_get_session(const mbedtls_ssl_context *ssl, * \return #MBEDTLS_ERR_SSL_HELLO_VERIFY_REQUIRED if DTLS is in use * and the client did not demonstrate reachability yet - in * this case you must stop using the context (see below). + * \return #MBEDTLS_ERR_SSL_RECEIVED_NEW_SESSION_TICKET if a TLS 1.3 + * NewSessionTicket message has been received. See the + * documentation of mbedtls_ssl_read() for more information + * about this error code. * \return #MBEDTLS_ERR_SSL_RECEIVED_EARLY_DATA if early data, as * defined in RFC 8446 (TLS 1.3 specification), has been * received as part of the handshake. This is server specific @@ -4901,6 +4942,7 @@ int mbedtls_ssl_get_session(const mbedtls_ssl_context *ssl, * #MBEDTLS_ERR_SSL_WANT_WRITE, * #MBEDTLS_ERR_SSL_ASYNC_IN_PROGRESS or * #MBEDTLS_ERR_SSL_CRYPTO_IN_PROGRESS or + * #MBEDTLS_ERR_SSL_RECEIVED_NEW_SESSION_TICKET or * #MBEDTLS_ERR_SSL_RECEIVED_EARLY_DATA, * you must stop using the SSL context for reading or writing, * and either free it or call \c mbedtls_ssl_session_reset() @@ -4921,10 +4963,13 @@ int mbedtls_ssl_get_session(const mbedtls_ssl_context *ssl, * currently being processed might or might not contain further * DTLS records. * - * \note If the context is configured to allow TLS 1.3, or if - * #MBEDTLS_USE_PSA_CRYPTO is enabled, the PSA crypto + * \note If #MBEDTLS_USE_PSA_CRYPTO is enabled, the PSA crypto * subsystem must have been initialized by calling * psa_crypto_init() before calling this function. + * Otherwise, the handshake may call psa_crypto_init() + * if a negotiation involving TLS 1.3 takes place (this may + * be the case even if TLS 1.3 is offered but eventually + * not selected). */ int mbedtls_ssl_handshake(mbedtls_ssl_context *ssl); @@ -4972,6 +5017,7 @@ static inline int mbedtls_ssl_is_handshake_over(mbedtls_ssl_context *ssl) * #MBEDTLS_ERR_SSL_WANT_READ, #MBEDTLS_ERR_SSL_WANT_WRITE, * #MBEDTLS_ERR_SSL_ASYNC_IN_PROGRESS, * #MBEDTLS_ERR_SSL_CRYPTO_IN_PROGRESS or + * #MBEDTLS_ERR_SSL_RECEIVED_NEW_SESSION_TICKET or * #MBEDTLS_ERR_SSL_RECEIVED_EARLY_DATA, you must stop using * the SSL context for reading or writing, and either free it * or call \c mbedtls_ssl_session_reset() on it before @@ -5040,6 +5086,17 @@ int mbedtls_ssl_renegotiate(mbedtls_ssl_context *ssl); * \return #MBEDTLS_ERR_SSL_CLIENT_RECONNECT if we're at the server * side of a DTLS connection and the client is initiating a * new connection using the same source port. See below. + * \return #MBEDTLS_ERR_SSL_RECEIVED_NEW_SESSION_TICKET if a TLS 1.3 + * NewSessionTicket message has been received. + * This error code is only returned on the client side. It is + * only returned if handling of TLS 1.3 NewSessionTicket + * messages has been enabled through + * mbedtls_ssl_conf_tls13_enable_signal_new_session_tickets(). + * This error code indicates that a TLS 1.3 NewSessionTicket + * message has been received and parsed successfully by the + * client. The ticket data can be retrieved from the SSL + * context by calling mbedtls_ssl_get_session(). It remains + * available until the next call to mbedtls_ssl_read(). * \return #MBEDTLS_ERR_SSL_RECEIVED_EARLY_DATA if early data, as * defined in RFC 8446 (TLS 1.3 specification), has been * received as part of the handshake. This is server specific @@ -5057,6 +5114,7 @@ int mbedtls_ssl_renegotiate(mbedtls_ssl_context *ssl); * #MBEDTLS_ERR_SSL_ASYNC_IN_PROGRESS, * #MBEDTLS_ERR_SSL_CRYPTO_IN_PROGRESS, * #MBEDTLS_ERR_SSL_CLIENT_RECONNECT or + * #MBEDTLS_ERR_SSL_RECEIVED_NEW_SESSION_TICKET or * #MBEDTLS_ERR_SSL_RECEIVED_EARLY_DATA, * you must stop using the SSL context for reading or writing, * and either free it or call \c mbedtls_ssl_session_reset() @@ -5122,6 +5180,10 @@ int mbedtls_ssl_read(mbedtls_ssl_context *ssl, unsigned char *buf, size_t len); * operation is in progress (see mbedtls_ecp_set_max_ops()) - * in this case you must call this function again to complete * the handshake when you're done attending other tasks. + * \return #MBEDTLS_ERR_SSL_RECEIVED_NEW_SESSION_TICKET if a TLS 1.3 + * NewSessionTicket message has been received. See the + * documentation of mbedtls_ssl_read() for more information + * about this error code. * \return #MBEDTLS_ERR_SSL_RECEIVED_EARLY_DATA if early data, as * defined in RFC 8446 (TLS 1.3 specification), has been * received as part of the handshake. This is server specific @@ -5138,6 +5200,7 @@ int mbedtls_ssl_read(mbedtls_ssl_context *ssl, unsigned char *buf, size_t len); * #MBEDTLS_ERR_SSL_WANT_WRITE, * #MBEDTLS_ERR_SSL_ASYNC_IN_PROGRESS, * #MBEDTLS_ERR_SSL_CRYPTO_IN_PROGRESS or + * #MBEDTLS_ERR_SSL_RECEIVED_NEW_SESSION_TICKET or * #MBEDTLS_ERR_SSL_RECEIVED_EARLY_DATA, * you must stop using the SSL context for reading or writing, * and either free it or call \c mbedtls_ssl_session_reset() diff --git a/thirdparty/mbedtls/include/psa/crypto.h b/thirdparty/mbedtls/include/psa/crypto.h index 390534edf5..96baf8f3ed 100644 --- a/thirdparty/mbedtls/include/psa/crypto.h +++ b/thirdparty/mbedtls/include/psa/crypto.h @@ -121,8 +121,8 @@ static psa_key_attributes_t psa_key_attributes_init(void); * value in the structure. * The persistent key will be written to storage when the attribute * structure is passed to a key creation function such as - * psa_import_key(), psa_generate_key(), psa_generate_key_ext(), - * psa_key_derivation_output_key(), psa_key_derivation_output_key_ext() + * psa_import_key(), psa_generate_key(), psa_generate_key_custom(), + * psa_key_derivation_output_key(), psa_key_derivation_output_key_custom() * or psa_copy_key(). * * This function may be declared as `static` (i.e. without external @@ -131,6 +131,9 @@ static psa_key_attributes_t psa_key_attributes_init(void); * * \param[out] attributes The attribute structure to write to. * \param key The persistent identifier for the key. + * This can be any value in the range from + * #PSA_KEY_ID_USER_MIN to #PSA_KEY_ID_USER_MAX + * inclusive. */ static void psa_set_key_id(psa_key_attributes_t *attributes, mbedtls_svc_key_id_t key); @@ -166,8 +169,8 @@ static void mbedtls_set_key_owner_id(psa_key_attributes_t *attributes, * value in the structure. * The persistent key will be written to storage when the attribute * structure is passed to a key creation function such as - * psa_import_key(), psa_generate_key(), psa_generate_key_ext(), - * psa_key_derivation_output_key(), psa_key_derivation_output_key_ext() + * psa_import_key(), psa_generate_key(), psa_generate_key_custom(), + * psa_key_derivation_output_key(), psa_key_derivation_output_key_custom() * or psa_copy_key(). * * This function may be declared as `static` (i.e. without external @@ -875,7 +878,7 @@ psa_status_t psa_hash_compute(psa_algorithm_t alg, * such that #PSA_ALG_IS_HASH(\p alg) is true). * \param[in] input Buffer containing the message to hash. * \param input_length Size of the \p input buffer in bytes. - * \param[out] hash Buffer containing the expected hash value. + * \param[in] hash Buffer containing the expected hash value. * \param hash_length Size of the \p hash buffer in bytes. * * \retval #PSA_SUCCESS @@ -1230,7 +1233,7 @@ psa_status_t psa_mac_compute(mbedtls_svc_key_id_t key, * such that #PSA_ALG_IS_MAC(\p alg) is true). * \param[in] input Buffer containing the input message. * \param input_length Size of the \p input buffer in bytes. - * \param[out] mac Buffer containing the expected MAC value. + * \param[in] mac Buffer containing the expected MAC value. * \param mac_length Size of the \p mac buffer in bytes. * * \retval #PSA_SUCCESS @@ -2922,7 +2925,7 @@ psa_status_t psa_sign_message(mbedtls_svc_key_id_t key, * \p key. * \param[in] input The message whose signature is to be verified. * \param[in] input_length Size of the \p input buffer in bytes. - * \param[out] signature Buffer containing the signature to verify. + * \param[in] signature Buffer containing the signature to verify. * \param[in] signature_length Size of the \p signature buffer in bytes. * * \retval #PSA_SUCCESS \emptydescription @@ -3248,7 +3251,7 @@ static psa_key_derivation_operation_t psa_key_derivation_operation_init(void); * of or after providing inputs. For some algorithms, this step is mandatory * because the output depends on the maximum capacity. * -# To derive a key, call psa_key_derivation_output_key() or - * psa_key_derivation_output_key_ext(). + * psa_key_derivation_output_key_custom(). * To derive a byte string for a different purpose, call * psa_key_derivation_output_bytes(). * Successive calls to these functions use successive output bytes @@ -3471,7 +3474,7 @@ psa_status_t psa_key_derivation_input_integer( * \note Once all inputs steps are completed, the operations will allow: * - psa_key_derivation_output_bytes() if each input was either a direct input * or a key with #PSA_KEY_USAGE_DERIVE set; - * - psa_key_derivation_output_key() or psa_key_derivation_output_key_ext() + * - psa_key_derivation_output_key() or psa_key_derivation_output_key_custom() * if the input for step * #PSA_KEY_DERIVATION_INPUT_SECRET or #PSA_KEY_DERIVATION_INPUT_PASSWORD * was from a key slot with #PSA_KEY_USAGE_DERIVE and each other input was @@ -3721,9 +3724,9 @@ psa_status_t psa_key_derivation_output_bytes( * on the derived key based on the attributes and strength of the secret key. * * \note This function is equivalent to calling - * psa_key_derivation_output_key_ext() - * with the production parameters #PSA_KEY_PRODUCTION_PARAMETERS_INIT - * and `params_data_length == 0` (i.e. `params->data` is empty). + * psa_key_derivation_output_key_custom() + * with the custom production parameters #PSA_CUSTOM_KEY_PARAMETERS_INIT + * and `custom_data_length == 0` (i.e. `custom_data` is empty). * * \param[in] attributes The attributes for the new key. * If the key type to be created is @@ -3795,6 +3798,85 @@ psa_status_t psa_key_derivation_output_key( * the policy must be the same as in the current * operation. * \param[in,out] operation The key derivation operation object to read from. + * \param[in] custom Customization parameters for the key generation. + * When this is #PSA_CUSTOM_KEY_PARAMETERS_INIT + * with \p custom_data_length = 0, + * this function is equivalent to + * psa_key_derivation_output_key(). + * \param[in] custom_data Variable-length data associated with \c custom. + * \param custom_data_length + * Length of `custom_data` in bytes. + * \param[out] key On success, an identifier for the newly created + * key. For persistent keys, this is the key + * identifier defined in \p attributes. + * \c 0 on failure. + * + * \retval #PSA_SUCCESS + * Success. + * If the key is persistent, the key material and the key's metadata + * have been saved to persistent storage. + * \retval #PSA_ERROR_ALREADY_EXISTS + * This is an attempt to create a persistent key, and there is + * already a persistent key with the given identifier. + * \retval #PSA_ERROR_INSUFFICIENT_DATA + * There was not enough data to create the desired key. + * Note that in this case, no output is written to the output buffer. + * The operation's capacity is set to 0, thus subsequent calls to + * this function will not succeed, even with a smaller output buffer. + * \retval #PSA_ERROR_NOT_SUPPORTED + * The key type or key size is not supported, either by the + * implementation in general or in this particular location. + * \retval #PSA_ERROR_INVALID_ARGUMENT + * The provided key attributes are not valid for the operation. + * \retval #PSA_ERROR_NOT_PERMITTED + * The #PSA_KEY_DERIVATION_INPUT_SECRET or + * #PSA_KEY_DERIVATION_INPUT_PASSWORD input was not provided through a + * key; or one of the inputs was a key whose policy didn't allow + * #PSA_KEY_USAGE_DERIVE. + * \retval #PSA_ERROR_INSUFFICIENT_MEMORY \emptydescription + * \retval #PSA_ERROR_INSUFFICIENT_STORAGE \emptydescription + * \retval #PSA_ERROR_COMMUNICATION_FAILURE \emptydescription + * \retval #PSA_ERROR_HARDWARE_FAILURE \emptydescription + * \retval #PSA_ERROR_CORRUPTION_DETECTED \emptydescription + * \retval #PSA_ERROR_DATA_INVALID \emptydescription + * \retval #PSA_ERROR_DATA_CORRUPT \emptydescription + * \retval #PSA_ERROR_STORAGE_FAILURE \emptydescription + * \retval #PSA_ERROR_BAD_STATE + * The operation state is not valid (it must be active and completed + * all required input steps), or the library has not been previously + * initialized by psa_crypto_init(). + * It is implementation-dependent whether a failure to initialize + * results in this error code. + */ +psa_status_t psa_key_derivation_output_key_custom( + const psa_key_attributes_t *attributes, + psa_key_derivation_operation_t *operation, + const psa_custom_key_parameters_t *custom, + const uint8_t *custom_data, + size_t custom_data_length, + mbedtls_svc_key_id_t *key); + +#ifndef __cplusplus +/* Omitted when compiling in C++, because one of the parameters is a + * pointer to a struct with a flexible array member, and that is not + * standard C++. + * https://github.com/Mbed-TLS/mbedtls/issues/9020 + */ +/** Derive a key from an ongoing key derivation operation with custom + * production parameters. + * + * \note + * This is a deprecated variant of psa_key_derivation_output_key_custom(). + * It is equivalent except that the associated variable-length data + * is passed in `params->data` instead of a separate parameter. + * This function will be removed in a future version of Mbed TLS. + * + * \param[in] attributes The attributes for the new key. + * If the key type to be created is + * #PSA_KEY_TYPE_PASSWORD_HASH then the algorithm in + * the policy must be the same as in the current + * operation. + * \param[in,out] operation The key derivation operation object to read from. * \param[in] params Customization parameters for the key derivation. * When this is #PSA_KEY_PRODUCTION_PARAMETERS_INIT * with \p params_data_length = 0, @@ -3848,14 +3930,13 @@ psa_status_t psa_key_derivation_output_key( * It is implementation-dependent whether a failure to initialize * results in this error code. */ -#ifndef __cplusplus psa_status_t psa_key_derivation_output_key_ext( const psa_key_attributes_t *attributes, psa_key_derivation_operation_t *operation, const psa_key_production_parameters_t *params, size_t params_data_length, mbedtls_svc_key_id_t *key); -#endif +#endif /* !__cplusplus */ /** Compare output data from a key derivation operation to an expected value. * @@ -3881,8 +3962,8 @@ psa_status_t psa_key_derivation_output_key_ext( * psa_key_derivation_abort(). * * \param[in,out] operation The key derivation operation object to read from. - * \param[in] expected_output Buffer containing the expected derivation output. - * \param output_length Length of the expected output; this is also the + * \param[in] expected Buffer containing the expected derivation output. + * \param expected_length Length of the expected output; this is also the * number of bytes that will be read. * * \retval #PSA_SUCCESS \emptydescription @@ -3912,8 +3993,8 @@ psa_status_t psa_key_derivation_output_key_ext( */ psa_status_t psa_key_derivation_verify_bytes( psa_key_derivation_operation_t *operation, - const uint8_t *expected_output, - size_t output_length); + const uint8_t *expected, + size_t expected_length); /** Compare output data from a key derivation operation to an expected value * stored in a key object. @@ -3943,7 +4024,7 @@ psa_status_t psa_key_derivation_verify_bytes( * operation. The value of this key was likely * computed by a previous call to * psa_key_derivation_output_key() or - * psa_key_derivation_output_key_ext(). + * psa_key_derivation_output_key_custom(). * * \retval #PSA_SUCCESS \emptydescription * \retval #PSA_ERROR_INVALID_SIGNATURE @@ -4111,9 +4192,9 @@ psa_status_t psa_generate_random(uint8_t *output, * between 2^{n-1} and 2^n where n is the bit size specified in the * attributes. * - * \note This function is equivalent to calling psa_generate_key_ext() - * with the production parameters #PSA_KEY_PRODUCTION_PARAMETERS_INIT - * and `params_data_length == 0` (i.e. `params->data` is empty). + * \note This function is equivalent to calling psa_generate_key_custom() + * with the custom production parameters #PSA_CUSTOM_KEY_PARAMETERS_INIT + * and `custom_data_length == 0` (i.e. `custom_data` is empty). * * \param[in] attributes The attributes for the new key. * \param[out] key On success, an identifier for the newly created @@ -4153,7 +4234,7 @@ psa_status_t psa_generate_key(const psa_key_attributes_t *attributes, * See the description of psa_generate_key() for the operation of this * function with the default production parameters. In addition, this function * supports the following production customizations, described in more detail - * in the documentation of ::psa_key_production_parameters_t: + * in the documentation of ::psa_custom_key_parameters_t: * * - RSA keys: generation with a custom public exponent. * @@ -4161,6 +4242,64 @@ psa_status_t psa_generate_key(const psa_key_attributes_t *attributes, * versions of Mbed TLS. * * \param[in] attributes The attributes for the new key. + * \param[in] custom Customization parameters for the key generation. + * When this is #PSA_CUSTOM_KEY_PARAMETERS_INIT + * with \p custom_data_length = 0, + * this function is equivalent to + * psa_generate_key(). + * \param[in] custom_data Variable-length data associated with \c custom. + * \param custom_data_length + * Length of `custom_data` in bytes. + * \param[out] key On success, an identifier for the newly created + * key. For persistent keys, this is the key + * identifier defined in \p attributes. + * \c 0 on failure. + * + * \retval #PSA_SUCCESS + * Success. + * If the key is persistent, the key material and the key's metadata + * have been saved to persistent storage. + * \retval #PSA_ERROR_ALREADY_EXISTS + * This is an attempt to create a persistent key, and there is + * already a persistent key with the given identifier. + * \retval #PSA_ERROR_NOT_SUPPORTED \emptydescription + * \retval #PSA_ERROR_INVALID_ARGUMENT \emptydescription + * \retval #PSA_ERROR_INSUFFICIENT_MEMORY \emptydescription + * \retval #PSA_ERROR_INSUFFICIENT_ENTROPY \emptydescription + * \retval #PSA_ERROR_COMMUNICATION_FAILURE \emptydescription + * \retval #PSA_ERROR_HARDWARE_FAILURE \emptydescription + * \retval #PSA_ERROR_CORRUPTION_DETECTED \emptydescription + * \retval #PSA_ERROR_INSUFFICIENT_STORAGE \emptydescription + * \retval #PSA_ERROR_DATA_INVALID \emptydescription + * \retval #PSA_ERROR_DATA_CORRUPT \emptydescription + * \retval #PSA_ERROR_STORAGE_FAILURE \emptydescription + * \retval #PSA_ERROR_BAD_STATE + * The library has not been previously initialized by psa_crypto_init(). + * It is implementation-dependent whether a failure to initialize + * results in this error code. + */ +psa_status_t psa_generate_key_custom(const psa_key_attributes_t *attributes, + const psa_custom_key_parameters_t *custom, + const uint8_t *custom_data, + size_t custom_data_length, + mbedtls_svc_key_id_t *key); + +#ifndef __cplusplus +/* Omitted when compiling in C++, because one of the parameters is a + * pointer to a struct with a flexible array member, and that is not + * standard C++. + * https://github.com/Mbed-TLS/mbedtls/issues/9020 + */ +/** + * \brief Generate a key or key pair using custom production parameters. + * + * \note + * This is a deprecated variant of psa_key_derivation_output_key_custom(). + * It is equivalent except that the associated variable-length data + * is passed in `params->data` instead of a separate parameter. + * This function will be removed in a future version of Mbed TLS. + * + * \param[in] attributes The attributes for the new key. * \param[in] params Customization parameters for the key generation. * When this is #PSA_KEY_PRODUCTION_PARAMETERS_INIT * with \p params_data_length = 0, @@ -4196,12 +4335,11 @@ psa_status_t psa_generate_key(const psa_key_attributes_t *attributes, * It is implementation-dependent whether a failure to initialize * results in this error code. */ -#ifndef __cplusplus psa_status_t psa_generate_key_ext(const psa_key_attributes_t *attributes, const psa_key_production_parameters_t *params, size_t params_data_length, mbedtls_svc_key_id_t *key); -#endif +#endif /* !__cplusplus */ /**@}*/ diff --git a/thirdparty/mbedtls/include/psa/crypto_adjust_auto_enabled.h b/thirdparty/mbedtls/include/psa/crypto_adjust_auto_enabled.h index 63fb29e85b..3a2af15180 100644 --- a/thirdparty/mbedtls/include/psa/crypto_adjust_auto_enabled.h +++ b/thirdparty/mbedtls/include/psa/crypto_adjust_auto_enabled.h @@ -2,6 +2,8 @@ * \file psa/crypto_adjust_auto_enabled.h * \brief Adjust PSA configuration: enable always-on features * + * This is an internal header. Do not include it directly. + * * Always enable certain features which require a negligible amount of code * to implement, to avoid some edge cases in the configuration combinatorics. */ @@ -13,6 +15,14 @@ #ifndef PSA_CRYPTO_ADJUST_AUTO_ENABLED_H #define PSA_CRYPTO_ADJUST_AUTO_ENABLED_H +#if !defined(MBEDTLS_CONFIG_FILES_READ) +#error "Do not include psa/crypto_adjust_*.h manually! This can lead to problems, " \ + "up to and including runtime errors such as buffer overflows. " \ + "If you're trying to fix a complaint from check_config.h, just remove " \ + "it from your configuration file: since Mbed TLS 3.0, it is included " \ + "automatically at the right point." +#endif /* */ + #define PSA_WANT_KEY_TYPE_DERIVE 1 #define PSA_WANT_KEY_TYPE_PASSWORD 1 #define PSA_WANT_KEY_TYPE_PASSWORD_HASH 1 diff --git a/thirdparty/mbedtls/include/psa/crypto_adjust_config_dependencies.h b/thirdparty/mbedtls/include/psa/crypto_adjust_config_dependencies.h new file mode 100644 index 0000000000..92e9c4de28 --- /dev/null +++ b/thirdparty/mbedtls/include/psa/crypto_adjust_config_dependencies.h @@ -0,0 +1,51 @@ +/** + * \file psa/crypto_adjust_config_dependencies.h + * \brief Adjust PSA configuration by resolving some dependencies. + * + * This is an internal header. Do not include it directly. + * + * See docs/proposed/psa-conditional-inclusion-c.md. + * If the Mbed TLS implementation of a cryptographic mechanism A depends on a + * cryptographic mechanism B then if the cryptographic mechanism A is enabled + * and not accelerated enable B. Note that if A is enabled and accelerated, it + * is not necessary to enable B for A support. + */ +/* + * Copyright The Mbed TLS Contributors + * SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later + */ + +#ifndef PSA_CRYPTO_ADJUST_CONFIG_DEPENDENCIES_H +#define PSA_CRYPTO_ADJUST_CONFIG_DEPENDENCIES_H + +#if !defined(MBEDTLS_CONFIG_FILES_READ) +#error "Do not include psa/crypto_adjust_*.h manually! This can lead to problems, " \ + "up to and including runtime errors such as buffer overflows. " \ + "If you're trying to fix a complaint from check_config.h, just remove " \ + "it from your configuration file: since Mbed TLS 3.0, it is included " \ + "automatically at the right point." +#endif /* */ + +#if (defined(PSA_WANT_ALG_TLS12_PRF) && \ + !defined(MBEDTLS_PSA_ACCEL_ALG_TLS12_PRF)) || \ + (defined(PSA_WANT_ALG_TLS12_PSK_TO_MS) && \ + !defined(MBEDTLS_PSA_ACCEL_ALG_TLS12_PSK_TO_MS)) || \ + (defined(PSA_WANT_ALG_HKDF) && \ + !defined(MBEDTLS_PSA_ACCEL_ALG_HKDF)) || \ + (defined(PSA_WANT_ALG_HKDF_EXTRACT) && \ + !defined(MBEDTLS_PSA_ACCEL_ALG_HKDF_EXTRACT)) || \ + (defined(PSA_WANT_ALG_HKDF_EXPAND) && \ + !defined(MBEDTLS_PSA_ACCEL_ALG_HKDF_EXPAND)) || \ + (defined(PSA_WANT_ALG_PBKDF2_HMAC) && \ + !defined(MBEDTLS_PSA_ACCEL_ALG_PBKDF2_HMAC)) +#define PSA_WANT_ALG_HMAC 1 +#define PSA_WANT_KEY_TYPE_HMAC 1 +#endif + +#if (defined(PSA_WANT_ALG_PBKDF2_AES_CMAC_PRF_128) && \ + !defined(MBEDTLS_PSA_ACCEL_ALG_PBKDF2_AES_CMAC_PRF_128)) +#define PSA_WANT_KEY_TYPE_AES 1 +#define PSA_WANT_ALG_CMAC 1 +#endif + +#endif /* PSA_CRYPTO_ADJUST_CONFIG_DEPENDENCIES_H */ diff --git a/thirdparty/mbedtls/include/psa/crypto_adjust_config_key_pair_types.h b/thirdparty/mbedtls/include/psa/crypto_adjust_config_key_pair_types.h index 63afc0e402..cec39e01ce 100644 --- a/thirdparty/mbedtls/include/psa/crypto_adjust_config_key_pair_types.h +++ b/thirdparty/mbedtls/include/psa/crypto_adjust_config_key_pair_types.h @@ -2,6 +2,8 @@ * \file psa/crypto_adjust_config_key_pair_types.h * \brief Adjust PSA configuration for key pair types. * + * This is an internal header. Do not include it directly. + * * See docs/proposed/psa-conditional-inclusion-c.md. * - Support non-basic operations in a keypair type implicitly enables basic * support for that keypair type. @@ -19,6 +21,14 @@ #ifndef PSA_CRYPTO_ADJUST_KEYPAIR_TYPES_H #define PSA_CRYPTO_ADJUST_KEYPAIR_TYPES_H +#if !defined(MBEDTLS_CONFIG_FILES_READ) +#error "Do not include psa/crypto_adjust_*.h manually! This can lead to problems, " \ + "up to and including runtime errors such as buffer overflows. " \ + "If you're trying to fix a complaint from check_config.h, just remove " \ + "it from your configuration file: since Mbed TLS 3.0, it is included " \ + "automatically at the right point." +#endif /* */ + /***************************************************************** * ANYTHING -> BASIC ****************************************************************/ diff --git a/thirdparty/mbedtls/include/psa/crypto_adjust_config_synonyms.h b/thirdparty/mbedtls/include/psa/crypto_adjust_config_synonyms.h index 332b622c9b..54b116f434 100644 --- a/thirdparty/mbedtls/include/psa/crypto_adjust_config_synonyms.h +++ b/thirdparty/mbedtls/include/psa/crypto_adjust_config_synonyms.h @@ -2,6 +2,8 @@ * \file psa/crypto_adjust_config_synonyms.h * \brief Adjust PSA configuration: enable quasi-synonyms * + * This is an internal header. Do not include it directly. + * * When two features require almost the same code, we automatically enable * both when either one is requested, to reduce the combinatorics of * possible configurations. @@ -14,6 +16,14 @@ #ifndef PSA_CRYPTO_ADJUST_CONFIG_SYNONYMS_H #define PSA_CRYPTO_ADJUST_CONFIG_SYNONYMS_H +#if !defined(MBEDTLS_CONFIG_FILES_READ) +#error "Do not include psa/crypto_adjust_*.h manually! This can lead to problems, " \ + "up to and including runtime errors such as buffer overflows. " \ + "If you're trying to fix a complaint from check_config.h, just remove " \ + "it from your configuration file: since Mbed TLS 3.0, it is included " \ + "automatically at the right point." +#endif /* */ + /****************************************************************/ /* De facto synonyms */ /****************************************************************/ diff --git a/thirdparty/mbedtls/include/psa/crypto_extra.h b/thirdparty/mbedtls/include/psa/crypto_extra.h index bd985e9b78..d276cd4c7f 100644 --- a/thirdparty/mbedtls/include/psa/crypto_extra.h +++ b/thirdparty/mbedtls/include/psa/crypto_extra.h @@ -154,6 +154,14 @@ static inline void psa_clear_key_slot_number( * specified in \p attributes. * * \param[in] attributes The attributes of the existing key. + * - The lifetime must be a persistent lifetime + * in a secure element. Volatile lifetimes are + * not currently supported. + * - The key identifier must be in the valid + * range for persistent keys. + * - The key type and size must be specified and + * must be consistent with the key material + * in the secure element. * * \retval #PSA_SUCCESS * The key was successfully registered. @@ -479,7 +487,7 @@ psa_status_t mbedtls_psa_external_get_random( * #PSA_KEY_ID_VENDOR_MIN and #PSA_KEY_ID_VENDOR_MAX and must not intersect * with any other set of implementation-chosen key identifiers. * - * This value is part of the library's ABI since changing it would invalidate + * This value is part of the library's API since changing it would invalidate * the values of built-in key identifiers in applications. */ #define MBEDTLS_PSA_KEY_ID_BUILTIN_MIN ((psa_key_id_t) 0x7fff0000) diff --git a/thirdparty/mbedtls/include/psa/crypto_struct.h b/thirdparty/mbedtls/include/psa/crypto_struct.h index e2c227b2eb..362e921a36 100644 --- a/thirdparty/mbedtls/include/psa/crypto_struct.h +++ b/thirdparty/mbedtls/include/psa/crypto_struct.h @@ -223,13 +223,36 @@ static inline struct psa_key_derivation_s psa_key_derivation_operation_init( return v; } +struct psa_custom_key_parameters_s { + /* Future versions may add other fields in this structure. */ + uint32_t flags; +}; + +/** The default production parameters for key generation or key derivation. + * + * Calling psa_generate_key_custom() or psa_key_derivation_output_key_custom() + * with `custom=PSA_CUSTOM_KEY_PARAMETERS_INIT` and `custom_data_length=0` is + * equivalent to calling psa_generate_key() or psa_key_derivation_output_key() + * respectively. + */ +#define PSA_CUSTOM_KEY_PARAMETERS_INIT { 0 } + #ifndef __cplusplus +/* Omitted when compiling in C++, because one of the parameters is a + * pointer to a struct with a flexible array member, and that is not + * standard C++. + * https://github.com/Mbed-TLS/mbedtls/issues/9020 + */ +/* This is a deprecated variant of `struct psa_custom_key_parameters_s`. + * It has exactly the same layout, plus an extra field which is a flexible + * array member. Thus a `const struct psa_key_production_parameters_s *` + * can be passed to any function that reads a + * `const struct psa_custom_key_parameters_s *`. + */ struct psa_key_production_parameters_s { - /* Future versions may add other fields in this structure. */ uint32_t flags; uint8_t data[]; }; -#endif /** The default production parameters for key generation or key derivation. * @@ -240,6 +263,7 @@ struct psa_key_production_parameters_s { * respectively. */ #define PSA_KEY_PRODUCTION_PARAMETERS_INIT { 0 } +#endif /* !__cplusplus */ struct psa_key_policy_s { psa_key_usage_t MBEDTLS_PRIVATE(usage); diff --git a/thirdparty/mbedtls/include/psa/crypto_types.h b/thirdparty/mbedtls/include/psa/crypto_types.h index a36b6ee65d..f831486f4e 100644 --- a/thirdparty/mbedtls/include/psa/crypto_types.h +++ b/thirdparty/mbedtls/include/psa/crypto_types.h @@ -457,6 +457,30 @@ typedef uint16_t psa_key_derivation_step_t; /** \brief Custom parameters for key generation or key derivation. * + * This is a structure type with at least the following field: + * + * - \c flags: an unsigned integer type. 0 for the default production parameters. + * + * Functions that take such a structure as input also take an associated + * input buffer \c custom_data of length \c custom_data_length. + * + * The interpretation of this structure and the associated \c custom_data + * parameter depend on the type of the created key. + * + * - #PSA_KEY_TYPE_RSA_KEY_PAIR: + * - \c flags: must be 0. + * - \c custom_data: the public exponent, in little-endian order. + * This must be an odd integer and must not be 1. + * Implementations must support 65537, should support 3 and may + * support other values. + * When not using a driver, Mbed TLS supports values up to \c INT_MAX. + * If this is empty, the default value 65537 is used. + * - Other key types: reserved for future use. \c flags must be 0. + */ +typedef struct psa_custom_key_parameters_s psa_custom_key_parameters_t; + +/** \brief Custom parameters for key generation or key derivation. + * * This is a structure type with at least the following fields: * * - \c flags: an unsigned integer type. 0 for the default production parameters. @@ -477,9 +501,7 @@ typedef uint16_t psa_key_derivation_step_t; * - Other key types: reserved for future use. \c flags must be 0. * */ -#ifndef __cplusplus typedef struct psa_key_production_parameters_s psa_key_production_parameters_t; -#endif /**@}*/ diff --git a/thirdparty/mbedtls/library/bignum.c b/thirdparty/mbedtls/library/bignum.c index c45fd5bf24..424490951d 100644 --- a/thirdparty/mbedtls/library/bignum.c +++ b/thirdparty/mbedtls/library/bignum.c @@ -27,6 +27,7 @@ #include "mbedtls/bignum.h" #include "bignum_core.h" +#include "bignum_internal.h" #include "bn_mul.h" #include "mbedtls/platform_util.h" #include "mbedtls/error.h" @@ -1610,9 +1611,13 @@ int mbedtls_mpi_mod_int(mbedtls_mpi_uint *r, const mbedtls_mpi *A, mbedtls_mpi_s return 0; } -int mbedtls_mpi_exp_mod(mbedtls_mpi *X, const mbedtls_mpi *A, - const mbedtls_mpi *E, const mbedtls_mpi *N, - mbedtls_mpi *prec_RR) +/* + * Warning! If the parameter E_public has MBEDTLS_MPI_IS_PUBLIC as its value, + * this function is not constant time with respect to the exponent (parameter E). + */ +static int mbedtls_mpi_exp_mod_optionally_safe(mbedtls_mpi *X, const mbedtls_mpi *A, + const mbedtls_mpi *E, int E_public, + const mbedtls_mpi *N, mbedtls_mpi *prec_RR) { int ret = MBEDTLS_ERR_ERROR_CORRUPTION_DETECTED; @@ -1695,7 +1700,11 @@ int mbedtls_mpi_exp_mod(mbedtls_mpi *X, const mbedtls_mpi *A, { mbedtls_mpi_uint mm = mbedtls_mpi_core_montmul_init(N->p); mbedtls_mpi_core_to_mont_rep(X->p, X->p, N->p, N->n, mm, RR.p, T); - mbedtls_mpi_core_exp_mod(X->p, X->p, N->p, N->n, E->p, E->n, RR.p, T); + if (E_public == MBEDTLS_MPI_IS_PUBLIC) { + mbedtls_mpi_core_exp_mod_unsafe(X->p, X->p, N->p, N->n, E->p, E->n, RR.p, T); + } else { + mbedtls_mpi_core_exp_mod(X->p, X->p, N->p, N->n, E->p, E->n, RR.p, T); + } mbedtls_mpi_core_from_mont_rep(X->p, X->p, N->p, N->n, mm, T); } @@ -1720,6 +1729,20 @@ cleanup: return ret; } +int mbedtls_mpi_exp_mod(mbedtls_mpi *X, const mbedtls_mpi *A, + const mbedtls_mpi *E, const mbedtls_mpi *N, + mbedtls_mpi *prec_RR) +{ + return mbedtls_mpi_exp_mod_optionally_safe(X, A, E, MBEDTLS_MPI_IS_SECRET, N, prec_RR); +} + +int mbedtls_mpi_exp_mod_unsafe(mbedtls_mpi *X, const mbedtls_mpi *A, + const mbedtls_mpi *E, const mbedtls_mpi *N, + mbedtls_mpi *prec_RR) +{ + return mbedtls_mpi_exp_mod_optionally_safe(X, A, E, MBEDTLS_MPI_IS_PUBLIC, N, prec_RR); +} + /* * Greatest common divisor: G = gcd(A, B) (HAC 14.54) */ diff --git a/thirdparty/mbedtls/library/bignum_core.c b/thirdparty/mbedtls/library/bignum_core.c index 1a3e0b9b6f..4231554b84 100644 --- a/thirdparty/mbedtls/library/bignum_core.c +++ b/thirdparty/mbedtls/library/bignum_core.c @@ -746,8 +746,94 @@ static void exp_mod_precompute_window(const mbedtls_mpi_uint *A, } } +#if defined(MBEDTLS_TEST_HOOKS) && !defined(MBEDTLS_THREADING_C) +// Set to a default that is neither MBEDTLS_MPI_IS_PUBLIC nor MBEDTLS_MPI_IS_SECRET +int mbedtls_mpi_optionally_safe_codepath = MBEDTLS_MPI_IS_PUBLIC + MBEDTLS_MPI_IS_SECRET + 1; +#endif + +/* + * This function calculates the indices of the exponent where the exponentiation algorithm should + * start processing. + * + * Warning! If the parameter E_public has MBEDTLS_MPI_IS_PUBLIC as its value, + * this function is not constant time with respect to the exponent (parameter E). + */ +static inline void exp_mod_calc_first_bit_optionally_safe(const mbedtls_mpi_uint *E, + size_t E_limbs, + int E_public, + size_t *E_limb_index, + size_t *E_bit_index) +{ + if (E_public == MBEDTLS_MPI_IS_PUBLIC) { + /* + * Skip leading zero bits. + */ + size_t E_bits = mbedtls_mpi_core_bitlen(E, E_limbs); + if (E_bits == 0) { + /* + * If E is 0 mbedtls_mpi_core_bitlen() returns 0. Even if that is the case, we will want + * to represent it as a single 0 bit and as such the bitlength will be 1. + */ + E_bits = 1; + } + + *E_limb_index = E_bits / biL; + *E_bit_index = E_bits % biL; + +#if defined(MBEDTLS_TEST_HOOKS) && !defined(MBEDTLS_THREADING_C) + mbedtls_mpi_optionally_safe_codepath = MBEDTLS_MPI_IS_PUBLIC; +#endif + } else { + /* + * Here we need to be constant time with respect to E and can't do anything better than + * start at the first allocated bit. + */ + *E_limb_index = E_limbs; + *E_bit_index = 0; +#if defined(MBEDTLS_TEST_HOOKS) && !defined(MBEDTLS_THREADING_C) + // Only mark the codepath safe if there wasn't an unsafe codepath before + if (mbedtls_mpi_optionally_safe_codepath != MBEDTLS_MPI_IS_PUBLIC) { + mbedtls_mpi_optionally_safe_codepath = MBEDTLS_MPI_IS_SECRET; + } +#endif + } +} + +/* + * Warning! If the parameter window_public has MBEDTLS_MPI_IS_PUBLIC as its value, this function is + * not constant time with respect to the window parameter and consequently the exponent of the + * exponentiation (parameter E of mbedtls_mpi_core_exp_mod_optionally_safe). + */ +static inline void exp_mod_table_lookup_optionally_safe(mbedtls_mpi_uint *Wselect, + mbedtls_mpi_uint *Wtable, + size_t AN_limbs, size_t welem, + mbedtls_mpi_uint window, + int window_public) +{ + if (window_public == MBEDTLS_MPI_IS_PUBLIC) { + memcpy(Wselect, Wtable + window * AN_limbs, AN_limbs * ciL); +#if defined(MBEDTLS_TEST_HOOKS) && !defined(MBEDTLS_THREADING_C) + mbedtls_mpi_optionally_safe_codepath = MBEDTLS_MPI_IS_PUBLIC; +#endif + } else { + /* Select Wtable[window] without leaking window through + * memory access patterns. */ + mbedtls_mpi_core_ct_uint_table_lookup(Wselect, Wtable, + AN_limbs, welem, window); +#if defined(MBEDTLS_TEST_HOOKS) && !defined(MBEDTLS_THREADING_C) + // Only mark the codepath safe if there wasn't an unsafe codepath before + if (mbedtls_mpi_optionally_safe_codepath != MBEDTLS_MPI_IS_PUBLIC) { + mbedtls_mpi_optionally_safe_codepath = MBEDTLS_MPI_IS_SECRET; + } +#endif + } +} + /* Exponentiation: X := A^E mod N. * + * Warning! If the parameter E_public has MBEDTLS_MPI_IS_PUBLIC as its value, + * this function is not constant time with respect to the exponent (parameter E). + * * A must already be in Montgomery form. * * As in other bignum functions, assume that AN_limbs and E_limbs are nonzero. @@ -758,16 +844,25 @@ static void exp_mod_precompute_window(const mbedtls_mpi_uint *A, * (The difference is that the body in our loop processes a single bit instead * of a full window.) */ -void mbedtls_mpi_core_exp_mod(mbedtls_mpi_uint *X, - const mbedtls_mpi_uint *A, - const mbedtls_mpi_uint *N, - size_t AN_limbs, - const mbedtls_mpi_uint *E, - size_t E_limbs, - const mbedtls_mpi_uint *RR, - mbedtls_mpi_uint *T) +static void mbedtls_mpi_core_exp_mod_optionally_safe(mbedtls_mpi_uint *X, + const mbedtls_mpi_uint *A, + const mbedtls_mpi_uint *N, + size_t AN_limbs, + const mbedtls_mpi_uint *E, + size_t E_limbs, + int E_public, + const mbedtls_mpi_uint *RR, + mbedtls_mpi_uint *T) { - const size_t wsize = exp_mod_get_window_size(E_limbs * biL); + /* We'll process the bits of E from most significant + * (limb_index=E_limbs-1, E_bit_index=biL-1) to least significant + * (limb_index=0, E_bit_index=0). */ + size_t E_limb_index; + size_t E_bit_index; + exp_mod_calc_first_bit_optionally_safe(E, E_limbs, E_public, + &E_limb_index, &E_bit_index); + + const size_t wsize = exp_mod_get_window_size(E_limb_index * biL); const size_t welem = ((size_t) 1) << wsize; /* This is how we will use the temporary storage T, which must have space @@ -786,7 +881,7 @@ void mbedtls_mpi_core_exp_mod(mbedtls_mpi_uint *X, const mbedtls_mpi_uint mm = mbedtls_mpi_core_montmul_init(N); - /* Set Wtable[i] = A^(2^i) (in Montgomery representation) */ + /* Set Wtable[i] = A^i (in Montgomery representation) */ exp_mod_precompute_window(A, N, AN_limbs, mm, RR, welem, Wtable, temp); @@ -798,11 +893,6 @@ void mbedtls_mpi_core_exp_mod(mbedtls_mpi_uint *X, /* X = 1 (in Montgomery presentation) initially */ memcpy(X, Wtable, AN_limbs * ciL); - /* We'll process the bits of E from most significant - * (limb_index=E_limbs-1, E_bit_index=biL-1) to least significant - * (limb_index=0, E_bit_index=0). */ - size_t E_limb_index = E_limbs; - size_t E_bit_index = 0; /* At any given time, window contains window_bits bits from E. * window_bits can go up to wsize. */ size_t window_bits = 0; @@ -828,10 +918,9 @@ void mbedtls_mpi_core_exp_mod(mbedtls_mpi_uint *X, * when we've finished processing the exponent. */ if (window_bits == wsize || (E_bit_index == 0 && E_limb_index == 0)) { - /* Select Wtable[window] without leaking window through - * memory access patterns. */ - mbedtls_mpi_core_ct_uint_table_lookup(Wselect, Wtable, - AN_limbs, welem, window); + + exp_mod_table_lookup_optionally_safe(Wselect, Wtable, AN_limbs, welem, + window, E_public); /* Multiply X by the selected element. */ mbedtls_mpi_core_montmul(X, X, Wselect, AN_limbs, N, AN_limbs, mm, temp); @@ -841,6 +930,42 @@ void mbedtls_mpi_core_exp_mod(mbedtls_mpi_uint *X, } while (!(E_bit_index == 0 && E_limb_index == 0)); } +void mbedtls_mpi_core_exp_mod(mbedtls_mpi_uint *X, + const mbedtls_mpi_uint *A, + const mbedtls_mpi_uint *N, size_t AN_limbs, + const mbedtls_mpi_uint *E, size_t E_limbs, + const mbedtls_mpi_uint *RR, + mbedtls_mpi_uint *T) +{ + mbedtls_mpi_core_exp_mod_optionally_safe(X, + A, + N, + AN_limbs, + E, + E_limbs, + MBEDTLS_MPI_IS_SECRET, + RR, + T); +} + +void mbedtls_mpi_core_exp_mod_unsafe(mbedtls_mpi_uint *X, + const mbedtls_mpi_uint *A, + const mbedtls_mpi_uint *N, size_t AN_limbs, + const mbedtls_mpi_uint *E, size_t E_limbs, + const mbedtls_mpi_uint *RR, + mbedtls_mpi_uint *T) +{ + mbedtls_mpi_core_exp_mod_optionally_safe(X, + A, + N, + AN_limbs, + E, + E_limbs, + MBEDTLS_MPI_IS_PUBLIC, + RR, + T); +} + mbedtls_mpi_uint mbedtls_mpi_core_sub_int(mbedtls_mpi_uint *X, const mbedtls_mpi_uint *A, mbedtls_mpi_uint c, /* doubles as carry */ diff --git a/thirdparty/mbedtls/library/bignum_core.h b/thirdparty/mbedtls/library/bignum_core.h index 92c8d47db5..cf6485a148 100644 --- a/thirdparty/mbedtls/library/bignum_core.h +++ b/thirdparty/mbedtls/library/bignum_core.h @@ -90,6 +90,27 @@ #define GET_BYTE(X, i) \ (((X)[(i) / ciL] >> (((i) % ciL) * 8)) & 0xff) +/* Constants to identify whether a value is public or secret. If a parameter is marked as secret by + * this constant, the function must be constant time with respect to the parameter. + * + * This is only needed for functions with the _optionally_safe postfix. All other functions have + * fixed behavior that can't be changed at runtime and are constant time with respect to their + * parameters as prescribed by their documentation or by conventions in their module's documentation. + * + * Parameters should be named X_public where X is the name of the + * corresponding input parameter. + * + * Implementation should always check using + * if (X_public == MBEDTLS_MPI_IS_PUBLIC) { + * // unsafe path + * } else { + * // safe path + * } + * not the other way round, in order to prevent misuse. (This is, if a value + * other than the two below is passed, default to the safe path.) */ +#define MBEDTLS_MPI_IS_PUBLIC 0x2a2a2a2a +#define MBEDTLS_MPI_IS_SECRET 0 + /** Count leading zero bits in a given integer. * * \warning The result is undefined if \p a == 0 @@ -605,6 +626,42 @@ int mbedtls_mpi_core_random(mbedtls_mpi_uint *X, size_t mbedtls_mpi_core_exp_mod_working_limbs(size_t AN_limbs, size_t E_limbs); /** + * \brief Perform a modular exponentiation with public or secret exponent: + * X = A^E mod N, where \p A is already in Montgomery form. + * + * \warning This function is not constant time with respect to \p E (the exponent). + * + * \p X may be aliased to \p A, but not to \p RR or \p E, even if \p E_limbs == + * \p AN_limbs. + * + * \param[out] X The destination MPI, as a little endian array of length + * \p AN_limbs. + * \param[in] A The base MPI, as a little endian array of length \p AN_limbs. + * Must be in Montgomery form. + * \param[in] N The modulus, as a little endian array of length \p AN_limbs. + * \param AN_limbs The number of limbs in \p X, \p A, \p N, \p RR. + * \param[in] E The exponent, as a little endian array of length \p E_limbs. + * \param E_limbs The number of limbs in \p E. + * \param[in] RR The precomputed residue of 2^{2*biL} modulo N, as a little + * endian array of length \p AN_limbs. + * \param[in,out] T Temporary storage of at least the number of limbs returned + * by `mbedtls_mpi_core_exp_mod_working_limbs()`. + * Its initial content is unused and its final content is + * indeterminate. + * It must not alias or otherwise overlap any of the other + * parameters. + * It is up to the caller to zeroize \p T when it is no + * longer needed, and before freeing it if it was dynamically + * allocated. + */ +void mbedtls_mpi_core_exp_mod_unsafe(mbedtls_mpi_uint *X, + const mbedtls_mpi_uint *A, + const mbedtls_mpi_uint *N, size_t AN_limbs, + const mbedtls_mpi_uint *E, size_t E_limbs, + const mbedtls_mpi_uint *RR, + mbedtls_mpi_uint *T); + +/** * \brief Perform a modular exponentiation with secret exponent: * X = A^E mod N, where \p A is already in Montgomery form. * @@ -760,4 +817,17 @@ void mbedtls_mpi_core_from_mont_rep(mbedtls_mpi_uint *X, mbedtls_mpi_uint mm, mbedtls_mpi_uint *T); +/* + * Can't define thread local variables with our abstraction layer: do nothing if threading is on. + */ +#if defined(MBEDTLS_TEST_HOOKS) && !defined(MBEDTLS_THREADING_C) +extern int mbedtls_mpi_optionally_safe_codepath; + +static inline void mbedtls_mpi_optionally_safe_codepath_reset(void) +{ + // Set to a default that is neither MBEDTLS_MPI_IS_PUBLIC nor MBEDTLS_MPI_IS_SECRET + mbedtls_mpi_optionally_safe_codepath = MBEDTLS_MPI_IS_PUBLIC + MBEDTLS_MPI_IS_SECRET + 1; +} +#endif + #endif /* MBEDTLS_BIGNUM_CORE_H */ diff --git a/thirdparty/mbedtls/library/bignum_internal.h b/thirdparty/mbedtls/library/bignum_internal.h new file mode 100644 index 0000000000..aceaf55ea2 --- /dev/null +++ b/thirdparty/mbedtls/library/bignum_internal.h @@ -0,0 +1,50 @@ +/** + * \file bignum_internal.h + * + * \brief Internal-only bignum public-key cryptosystem API. + * + * This file declares bignum-related functions that are to be used + * only from within the Mbed TLS library itself. + * + */ +/* + * Copyright The Mbed TLS Contributors + * SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later + */ +#ifndef MBEDTLS_BIGNUM_INTERNAL_H +#define MBEDTLS_BIGNUM_INTERNAL_H + +/** + * \brief Perform a modular exponentiation: X = A^E mod N + * + * \warning This function is not constant time with respect to \p E (the exponent). + * + * \param X The destination MPI. This must point to an initialized MPI. + * This must not alias E or N. + * \param A The base of the exponentiation. + * This must point to an initialized MPI. + * \param E The exponent MPI. This must point to an initialized MPI. + * \param N The base for the modular reduction. This must point to an + * initialized MPI. + * \param prec_RR A helper MPI depending solely on \p N which can be used to + * speed-up multiple modular exponentiations for the same value + * of \p N. This may be \c NULL. If it is not \c NULL, it must + * point to an initialized MPI. If it hasn't been used after + * the call to mbedtls_mpi_init(), this function will compute + * the helper value and store it in \p prec_RR for reuse on + * subsequent calls to this function. Otherwise, the function + * will assume that \p prec_RR holds the helper value set by a + * previous call to mbedtls_mpi_exp_mod(), and reuse it. + * + * \return \c 0 if successful. + * \return #MBEDTLS_ERR_MPI_ALLOC_FAILED if a memory allocation failed. + * \return #MBEDTLS_ERR_MPI_BAD_INPUT_DATA if \c N is negative or + * even, or if \c E is negative. + * \return Another negative error code on different kinds of failures. + * + */ +int mbedtls_mpi_exp_mod_unsafe(mbedtls_mpi *X, const mbedtls_mpi *A, + const mbedtls_mpi *E, const mbedtls_mpi *N, + mbedtls_mpi *prec_RR); + +#endif /* bignum_internal.h */ diff --git a/thirdparty/mbedtls/library/block_cipher.c b/thirdparty/mbedtls/library/block_cipher.c index 04cd7fb444..51cdcdf46b 100644 --- a/thirdparty/mbedtls/library/block_cipher.c +++ b/thirdparty/mbedtls/library/block_cipher.c @@ -51,6 +51,10 @@ static int mbedtls_cipher_error_from_psa(psa_status_t status) void mbedtls_block_cipher_free(mbedtls_block_cipher_context_t *ctx) { + if (ctx == NULL) { + return; + } + #if defined(MBEDTLS_BLOCK_CIPHER_SOME_PSA) if (ctx->engine == MBEDTLS_BLOCK_CIPHER_ENGINE_PSA) { psa_destroy_key(ctx->psa_key_id); diff --git a/thirdparty/mbedtls/library/cipher.c b/thirdparty/mbedtls/library/cipher.c index 0683677eda..7f4c121492 100644 --- a/thirdparty/mbedtls/library/cipher.c +++ b/thirdparty/mbedtls/library/cipher.c @@ -849,6 +849,9 @@ static int get_pkcs_padding(unsigned char *input, size_t input_len, } padding_len = input[input_len - 1]; + if (padding_len == 0 || padding_len > input_len) { + return MBEDTLS_ERR_CIPHER_INVALID_PADDING; + } *data_len = input_len - padding_len; mbedtls_ct_condition_t bad = mbedtls_ct_uint_gt(padding_len, input_len); diff --git a/thirdparty/mbedtls/library/common.h b/thirdparty/mbedtls/library/common.h index 3936ffdfe1..7bb2674293 100644 --- a/thirdparty/mbedtls/library/common.h +++ b/thirdparty/mbedtls/library/common.h @@ -352,17 +352,19 @@ static inline void mbedtls_xor_no_simd(unsigned char *r, #endif /* Always provide a static assert macro, so it can be used unconditionally. - * It will expand to nothing on some systems. - * Can be used outside functions (but don't add a trailing ';' in that case: - * the semicolon is included here to avoid triggering -Wextra-semi when - * MBEDTLS_STATIC_ASSERT() expands to nothing). - * Can't use the C11-style `defined(static_assert)` on FreeBSD, since it + * It does nothing on systems where we don't know how to define a static assert. + */ +/* Can't use the C11-style `defined(static_assert)` on FreeBSD, since it * defines static_assert even with -std=c99, but then complains about it. */ #if defined(static_assert) && !defined(__FreeBSD__) -#define MBEDTLS_STATIC_ASSERT(expr, msg) static_assert(expr, msg); +#define MBEDTLS_STATIC_ASSERT(expr, msg) static_assert(expr, msg) #else -#define MBEDTLS_STATIC_ASSERT(expr, msg) +/* Make sure `MBEDTLS_STATIC_ASSERT(expr, msg);` is valid both inside and + * outside a function. We choose a struct declaration, which can be repeated + * any number of times and does not need a matching definition. */ +#define MBEDTLS_STATIC_ASSERT(expr, msg) \ + struct ISO_C_does_not_allow_extra_semicolon_outside_of_a_function #endif #if defined(__has_builtin) diff --git a/thirdparty/mbedtls/library/ctr_drbg.c b/thirdparty/mbedtls/library/ctr_drbg.c index 66d9d28c58..b82044eb7d 100644 --- a/thirdparty/mbedtls/library/ctr_drbg.c +++ b/thirdparty/mbedtls/library/ctr_drbg.c @@ -26,13 +26,13 @@ #endif /* Using error translation functions from PSA to MbedTLS */ -#if !defined(MBEDTLS_AES_C) +#if defined(MBEDTLS_CTR_DRBG_USE_PSA_CRYPTO) #include "psa_util_internal.h" #endif #include "mbedtls/platform.h" -#if !defined(MBEDTLS_AES_C) +#if defined(MBEDTLS_CTR_DRBG_USE_PSA_CRYPTO) static psa_status_t ctr_drbg_setup_psa_context(mbedtls_ctr_drbg_psa_context *psa_ctx, unsigned char *key, size_t key_len) { @@ -73,11 +73,11 @@ static void ctr_drbg_destroy_psa_contex(mbedtls_ctr_drbg_psa_context *psa_ctx) void mbedtls_ctr_drbg_init(mbedtls_ctr_drbg_context *ctx) { memset(ctx, 0, sizeof(mbedtls_ctr_drbg_context)); -#if defined(MBEDTLS_AES_C) - mbedtls_aes_init(&ctx->aes_ctx); -#else +#if defined(MBEDTLS_CTR_DRBG_USE_PSA_CRYPTO) ctx->psa_ctx.key_id = MBEDTLS_SVC_KEY_ID_INIT; ctx->psa_ctx.operation = psa_cipher_operation_init(); +#else + mbedtls_aes_init(&ctx->aes_ctx); #endif /* Indicate that the entropy nonce length is not set explicitly. * See mbedtls_ctr_drbg_set_nonce_len(). */ @@ -102,10 +102,10 @@ void mbedtls_ctr_drbg_free(mbedtls_ctr_drbg_context *ctx) mbedtls_mutex_free(&ctx->mutex); } #endif -#if defined(MBEDTLS_AES_C) - mbedtls_aes_free(&ctx->aes_ctx); -#else +#if defined(MBEDTLS_CTR_DRBG_USE_PSA_CRYPTO) ctr_drbg_destroy_psa_contex(&ctx->psa_ctx); +#else + mbedtls_aes_free(&ctx->aes_ctx); #endif mbedtls_platform_zeroize(ctx, sizeof(mbedtls_ctr_drbg_context)); ctx->reseed_interval = MBEDTLS_CTR_DRBG_RESEED_INTERVAL; @@ -168,15 +168,15 @@ static int block_cipher_df(unsigned char *output, unsigned char chain[MBEDTLS_CTR_DRBG_BLOCKSIZE]; unsigned char *p, *iv; int ret = 0; -#if defined(MBEDTLS_AES_C) - mbedtls_aes_context aes_ctx; -#else +#if defined(MBEDTLS_CTR_DRBG_USE_PSA_CRYPTO) psa_status_t status; size_t tmp_len; mbedtls_ctr_drbg_psa_context psa_ctx; psa_ctx.key_id = MBEDTLS_SVC_KEY_ID_INIT; psa_ctx.operation = psa_cipher_operation_init(); +#else + mbedtls_aes_context aes_ctx; #endif int i, j; @@ -209,19 +209,19 @@ static int block_cipher_df(unsigned char *output, key[i] = i; } -#if defined(MBEDTLS_AES_C) +#if defined(MBEDTLS_CTR_DRBG_USE_PSA_CRYPTO) + status = ctr_drbg_setup_psa_context(&psa_ctx, key, sizeof(key)); + if (status != PSA_SUCCESS) { + ret = psa_generic_status_to_mbedtls(status); + goto exit; + } +#else mbedtls_aes_init(&aes_ctx); if ((ret = mbedtls_aes_setkey_enc(&aes_ctx, key, MBEDTLS_CTR_DRBG_KEYBITS)) != 0) { goto exit; } -#else - status = ctr_drbg_setup_psa_context(&psa_ctx, key, sizeof(key)); - if (status != PSA_SUCCESS) { - ret = psa_generic_status_to_mbedtls(status); - goto exit; - } #endif /* @@ -238,18 +238,18 @@ static int block_cipher_df(unsigned char *output, use_len -= (use_len >= MBEDTLS_CTR_DRBG_BLOCKSIZE) ? MBEDTLS_CTR_DRBG_BLOCKSIZE : use_len; -#if defined(MBEDTLS_AES_C) - if ((ret = mbedtls_aes_crypt_ecb(&aes_ctx, MBEDTLS_AES_ENCRYPT, - chain, chain)) != 0) { - goto exit; - } -#else +#if defined(MBEDTLS_CTR_DRBG_USE_PSA_CRYPTO) status = psa_cipher_update(&psa_ctx.operation, chain, MBEDTLS_CTR_DRBG_BLOCKSIZE, chain, MBEDTLS_CTR_DRBG_BLOCKSIZE, &tmp_len); if (status != PSA_SUCCESS) { ret = psa_generic_status_to_mbedtls(status); goto exit; } +#else + if ((ret = mbedtls_aes_crypt_ecb(&aes_ctx, MBEDTLS_AES_ENCRYPT, + chain, chain)) != 0) { + goto exit; + } #endif } @@ -264,12 +264,7 @@ static int block_cipher_df(unsigned char *output, /* * Do final encryption with reduced data */ -#if defined(MBEDTLS_AES_C) - if ((ret = mbedtls_aes_setkey_enc(&aes_ctx, tmp, - MBEDTLS_CTR_DRBG_KEYBITS)) != 0) { - goto exit; - } -#else +#if defined(MBEDTLS_CTR_DRBG_USE_PSA_CRYPTO) ctr_drbg_destroy_psa_contex(&psa_ctx); status = ctr_drbg_setup_psa_context(&psa_ctx, tmp, MBEDTLS_CTR_DRBG_KEYSIZE); @@ -277,32 +272,37 @@ static int block_cipher_df(unsigned char *output, ret = psa_generic_status_to_mbedtls(status); goto exit; } +#else + if ((ret = mbedtls_aes_setkey_enc(&aes_ctx, tmp, + MBEDTLS_CTR_DRBG_KEYBITS)) != 0) { + goto exit; + } #endif iv = tmp + MBEDTLS_CTR_DRBG_KEYSIZE; p = output; for (j = 0; j < MBEDTLS_CTR_DRBG_SEEDLEN; j += MBEDTLS_CTR_DRBG_BLOCKSIZE) { -#if defined(MBEDTLS_AES_C) - if ((ret = mbedtls_aes_crypt_ecb(&aes_ctx, MBEDTLS_AES_ENCRYPT, - iv, iv)) != 0) { - goto exit; - } -#else +#if defined(MBEDTLS_CTR_DRBG_USE_PSA_CRYPTO) status = psa_cipher_update(&psa_ctx.operation, iv, MBEDTLS_CTR_DRBG_BLOCKSIZE, iv, MBEDTLS_CTR_DRBG_BLOCKSIZE, &tmp_len); if (status != PSA_SUCCESS) { ret = psa_generic_status_to_mbedtls(status); goto exit; } +#else + if ((ret = mbedtls_aes_crypt_ecb(&aes_ctx, MBEDTLS_AES_ENCRYPT, + iv, iv)) != 0) { + goto exit; + } #endif memcpy(p, iv, MBEDTLS_CTR_DRBG_BLOCKSIZE); p += MBEDTLS_CTR_DRBG_BLOCKSIZE; } exit: -#if defined(MBEDTLS_AES_C) - mbedtls_aes_free(&aes_ctx); -#else +#if defined(MBEDTLS_CTR_DRBG_USE_PSA_CRYPTO) ctr_drbg_destroy_psa_contex(&psa_ctx); +#else + mbedtls_aes_free(&aes_ctx); #endif /* * tidy up the stack @@ -336,7 +336,7 @@ static int ctr_drbg_update_internal(mbedtls_ctr_drbg_context *ctx, unsigned char *p = tmp; int j; int ret = 0; -#if !defined(MBEDTLS_AES_C) +#if defined(MBEDTLS_CTR_DRBG_USE_PSA_CRYPTO) psa_status_t status; size_t tmp_len; #endif @@ -352,18 +352,18 @@ static int ctr_drbg_update_internal(mbedtls_ctr_drbg_context *ctx, /* * Crypt counter block */ -#if defined(MBEDTLS_AES_C) - if ((ret = mbedtls_aes_crypt_ecb(&ctx->aes_ctx, MBEDTLS_AES_ENCRYPT, - ctx->counter, p)) != 0) { - goto exit; - } -#else +#if defined(MBEDTLS_CTR_DRBG_USE_PSA_CRYPTO) status = psa_cipher_update(&ctx->psa_ctx.operation, ctx->counter, sizeof(ctx->counter), p, MBEDTLS_CTR_DRBG_BLOCKSIZE, &tmp_len); if (status != PSA_SUCCESS) { ret = psa_generic_status_to_mbedtls(status); goto exit; } +#else + if ((ret = mbedtls_aes_crypt_ecb(&ctx->aes_ctx, MBEDTLS_AES_ENCRYPT, + ctx->counter, p)) != 0) { + goto exit; + } #endif p += MBEDTLS_CTR_DRBG_BLOCKSIZE; @@ -374,12 +374,7 @@ static int ctr_drbg_update_internal(mbedtls_ctr_drbg_context *ctx, /* * Update key and counter */ -#if defined(MBEDTLS_AES_C) - if ((ret = mbedtls_aes_setkey_enc(&ctx->aes_ctx, tmp, - MBEDTLS_CTR_DRBG_KEYBITS)) != 0) { - goto exit; - } -#else +#if defined(MBEDTLS_CTR_DRBG_USE_PSA_CRYPTO) ctr_drbg_destroy_psa_contex(&ctx->psa_ctx); status = ctr_drbg_setup_psa_context(&ctx->psa_ctx, tmp, MBEDTLS_CTR_DRBG_KEYSIZE); @@ -387,6 +382,11 @@ static int ctr_drbg_update_internal(mbedtls_ctr_drbg_context *ctx, ret = psa_generic_status_to_mbedtls(status); goto exit; } +#else + if ((ret = mbedtls_aes_setkey_enc(&ctx->aes_ctx, tmp, + MBEDTLS_CTR_DRBG_KEYBITS)) != 0) { + goto exit; + } #endif memcpy(ctx->counter, tmp + MBEDTLS_CTR_DRBG_KEYSIZE, MBEDTLS_CTR_DRBG_BLOCKSIZE); @@ -564,12 +564,7 @@ int mbedtls_ctr_drbg_seed(mbedtls_ctr_drbg_context *ctx, good_nonce_len(ctx->entropy_len)); /* Initialize with an empty key. */ -#if defined(MBEDTLS_AES_C) - if ((ret = mbedtls_aes_setkey_enc(&ctx->aes_ctx, key, - MBEDTLS_CTR_DRBG_KEYBITS)) != 0) { - return ret; - } -#else +#if defined(MBEDTLS_CTR_DRBG_USE_PSA_CRYPTO) psa_status_t status; status = ctr_drbg_setup_psa_context(&ctx->psa_ctx, key, MBEDTLS_CTR_DRBG_KEYSIZE); @@ -577,6 +572,11 @@ int mbedtls_ctr_drbg_seed(mbedtls_ctr_drbg_context *ctx, ret = psa_generic_status_to_mbedtls(status); return status; } +#else + if ((ret = mbedtls_aes_setkey_enc(&ctx->aes_ctx, key, + MBEDTLS_CTR_DRBG_KEYBITS)) != 0) { + return ret; + } #endif /* Do the initial seeding. */ @@ -655,12 +655,7 @@ int mbedtls_ctr_drbg_random_with_add(void *p_rng, /* * Crypt counter block */ -#if defined(MBEDTLS_AES_C) - if ((ret = mbedtls_aes_crypt_ecb(&ctx->aes_ctx, MBEDTLS_AES_ENCRYPT, - ctx->counter, locals.tmp)) != 0) { - goto exit; - } -#else +#if defined(MBEDTLS_CTR_DRBG_USE_PSA_CRYPTO) psa_status_t status; size_t tmp_len; @@ -670,6 +665,11 @@ int mbedtls_ctr_drbg_random_with_add(void *p_rng, ret = psa_generic_status_to_mbedtls(status); goto exit; } +#else + if ((ret = mbedtls_aes_crypt_ecb(&ctx->aes_ctx, MBEDTLS_AES_ENCRYPT, + ctx->counter, locals.tmp)) != 0) { + goto exit; + } #endif use_len = (output_len > MBEDTLS_CTR_DRBG_BLOCKSIZE) diff --git a/thirdparty/mbedtls/library/entropy.c b/thirdparty/mbedtls/library/entropy.c index e3bc8516e2..7dcf067a52 100644 --- a/thirdparty/mbedtls/library/entropy.c +++ b/thirdparty/mbedtls/library/entropy.c @@ -61,6 +61,10 @@ void mbedtls_entropy_init(mbedtls_entropy_context *ctx) void mbedtls_entropy_free(mbedtls_entropy_context *ctx) { + if (ctx == NULL) { + return; + } + /* If the context was already free, don't call free() again. * This is important for mutexes which don't allow double-free. */ if (ctx->accumulator_started == -1) { diff --git a/thirdparty/mbedtls/library/entropy_poll.c b/thirdparty/mbedtls/library/entropy_poll.c index 794ee03a83..611768cd85 100644 --- a/thirdparty/mbedtls/library/entropy_poll.c +++ b/thirdparty/mbedtls/library/entropy_poll.c @@ -5,10 +5,12 @@ * SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later */ -#if defined(__linux__) || defined(__midipix__) && !defined(_GNU_SOURCE) +#if defined(__linux__) || defined(__midipix__) /* Ensure that syscall() is available even when compiling with -std=c99 */ +#if !defined(_GNU_SOURCE) #define _GNU_SOURCE #endif +#endif #include "common.h" diff --git a/thirdparty/mbedtls/library/error.c b/thirdparty/mbedtls/library/error.c index 84b637aeb2..6ad7162ab5 100644 --- a/thirdparty/mbedtls/library/error.c +++ b/thirdparty/mbedtls/library/error.c @@ -418,7 +418,7 @@ const char *mbedtls_high_level_strerr(int error_code) case -(MBEDTLS_ERR_SSL_BAD_CERTIFICATE): return( "SSL - Processing of the Certificate handshake message failed" ); case -(MBEDTLS_ERR_SSL_RECEIVED_NEW_SESSION_TICKET): - return( "SSL - * Received NewSessionTicket Post Handshake Message. This error code is experimental and may be changed or removed without notice" ); + return( "SSL - A TLS 1.3 NewSessionTicket message has been received" ); case -(MBEDTLS_ERR_SSL_CANNOT_READ_EARLY_DATA): return( "SSL - Not possible to read early data" ); case -(MBEDTLS_ERR_SSL_RECEIVED_EARLY_DATA): diff --git a/thirdparty/mbedtls/library/lmots.c b/thirdparty/mbedtls/library/lmots.c index c7091b49e1..c51cb41ece 100644 --- a/thirdparty/mbedtls/library/lmots.c +++ b/thirdparty/mbedtls/library/lmots.c @@ -387,6 +387,10 @@ void mbedtls_lmots_public_init(mbedtls_lmots_public_t *ctx) void mbedtls_lmots_public_free(mbedtls_lmots_public_t *ctx) { + if (ctx == NULL) { + return; + } + mbedtls_platform_zeroize(ctx, sizeof(*ctx)); } @@ -556,6 +560,10 @@ void mbedtls_lmots_private_init(mbedtls_lmots_private_t *ctx) void mbedtls_lmots_private_free(mbedtls_lmots_private_t *ctx) { + if (ctx == NULL) { + return; + } + mbedtls_platform_zeroize(ctx, sizeof(*ctx)); } diff --git a/thirdparty/mbedtls/library/lms.c b/thirdparty/mbedtls/library/lms.c index 8d3cae0524..7f7bec068b 100644 --- a/thirdparty/mbedtls/library/lms.c +++ b/thirdparty/mbedtls/library/lms.c @@ -229,6 +229,10 @@ void mbedtls_lms_public_init(mbedtls_lms_public_t *ctx) void mbedtls_lms_public_free(mbedtls_lms_public_t *ctx) { + if (ctx == NULL) { + return; + } + mbedtls_platform_zeroize(ctx, sizeof(*ctx)); } @@ -528,6 +532,10 @@ void mbedtls_lms_private_init(mbedtls_lms_private_t *ctx) void mbedtls_lms_private_free(mbedtls_lms_private_t *ctx) { + if (ctx == NULL) { + return; + } + unsigned int idx; if (ctx->have_private_key) { diff --git a/thirdparty/mbedtls/library/md.c b/thirdparty/mbedtls/library/md.c index 12a3ea2374..c95846aa04 100644 --- a/thirdparty/mbedtls/library/md.c +++ b/thirdparty/mbedtls/library/md.c @@ -41,7 +41,7 @@ #include "mbedtls/sha512.h" #include "mbedtls/sha3.h" -#if defined(MBEDTLS_PSA_CRYPTO_C) +#if defined(MBEDTLS_PSA_CRYPTO_CLIENT) #include <psa/crypto.h> #include "md_psa.h" #include "psa_util_internal.h" @@ -761,13 +761,13 @@ mbedtls_md_type_t mbedtls_md_get_type(const mbedtls_md_info_t *md_info) return md_info->type; } -#if defined(MBEDTLS_PSA_CRYPTO_C) +#if defined(MBEDTLS_PSA_CRYPTO_CLIENT) int mbedtls_md_error_from_psa(psa_status_t status) { return PSA_TO_MBEDTLS_ERR_LIST(status, psa_to_md_errors, psa_generic_status_to_mbedtls); } -#endif /* MBEDTLS_PSA_CRYPTO_C */ +#endif /* MBEDTLS_PSA_CRYPTO_CLIENT */ /************************************************************************ diff --git a/thirdparty/mbedtls/library/net_sockets.c b/thirdparty/mbedtls/library/net_sockets.c index edec5876ad..ef89a88ef0 100644 --- a/thirdparty/mbedtls/library/net_sockets.c +++ b/thirdparty/mbedtls/library/net_sockets.c @@ -683,7 +683,7 @@ void mbedtls_net_close(mbedtls_net_context *ctx) */ void mbedtls_net_free(mbedtls_net_context *ctx) { - if (ctx->fd == -1) { + if (ctx == NULL || ctx->fd == -1) { return; } diff --git a/thirdparty/mbedtls/library/nist_kw.c b/thirdparty/mbedtls/library/nist_kw.c index f15425b8bd..8faafe43f1 100644 --- a/thirdparty/mbedtls/library/nist_kw.c +++ b/thirdparty/mbedtls/library/nist_kw.c @@ -102,6 +102,10 @@ int mbedtls_nist_kw_setkey(mbedtls_nist_kw_context *ctx, */ void mbedtls_nist_kw_free(mbedtls_nist_kw_context *ctx) { + if (ctx == NULL) { + return; + } + mbedtls_cipher_free(&ctx->cipher_ctx); mbedtls_platform_zeroize(ctx, sizeof(mbedtls_nist_kw_context)); } diff --git a/thirdparty/mbedtls/library/pem.c b/thirdparty/mbedtls/library/pem.c index 0fee5df43a..0207601456 100644 --- a/thirdparty/mbedtls/library/pem.c +++ b/thirdparty/mbedtls/library/pem.c @@ -481,6 +481,10 @@ int mbedtls_pem_read_buffer(mbedtls_pem_context *ctx, const char *header, const void mbedtls_pem_free(mbedtls_pem_context *ctx) { + if (ctx == NULL) { + return; + } + if (ctx->buf != NULL) { mbedtls_zeroize_and_free(ctx->buf, ctx->buflen); } diff --git a/thirdparty/mbedtls/library/pk.c b/thirdparty/mbedtls/library/pk.c index 097777f2c0..3fe51ea34f 100644 --- a/thirdparty/mbedtls/library/pk.c +++ b/thirdparty/mbedtls/library/pk.c @@ -868,7 +868,6 @@ static int copy_from_psa(mbedtls_svc_key_id_t key_id, psa_status_t status; psa_key_attributes_t key_attr = PSA_KEY_ATTRIBUTES_INIT; psa_key_type_t key_type; - psa_algorithm_t alg_type; size_t key_bits; /* Use a buffer size large enough to contain either a key pair or public key. */ unsigned char exp_key[PSA_EXPORT_KEY_PAIR_OR_PUBLIC_MAX_SIZE]; @@ -899,7 +898,6 @@ static int copy_from_psa(mbedtls_svc_key_id_t key_id, key_type = PSA_KEY_TYPE_PUBLIC_KEY_OF_KEY_PAIR(key_type); } key_bits = psa_get_key_bits(&key_attr); - alg_type = psa_get_key_algorithm(&key_attr); #if defined(MBEDTLS_RSA_C) if ((key_type == PSA_KEY_TYPE_RSA_KEY_PAIR) || @@ -919,6 +917,7 @@ static int copy_from_psa(mbedtls_svc_key_id_t key_id, goto exit; } + psa_algorithm_t alg_type = psa_get_key_algorithm(&key_attr); mbedtls_md_type_t md_type = MBEDTLS_MD_NONE; if (PSA_ALG_GET_HASH(alg_type) != PSA_ALG_ANY_HASH) { md_type = mbedtls_md_type_from_psa_alg(alg_type); @@ -968,6 +967,7 @@ static int copy_from_psa(mbedtls_svc_key_id_t key_id, } else #endif /* MBEDTLS_PK_HAVE_ECC_KEYS */ { + (void) key_bits; return MBEDTLS_ERR_PK_BAD_INPUT_DATA; } @@ -1327,43 +1327,19 @@ int mbedtls_pk_sign_ext(mbedtls_pk_type_t pk_type, } if (mbedtls_pk_get_type(ctx) == MBEDTLS_PK_OPAQUE) { - psa_key_attributes_t key_attr = PSA_KEY_ATTRIBUTES_INIT; - psa_algorithm_t psa_alg, sign_alg; -#if defined(MBEDTLS_PSA_CRYPTO_C) - psa_algorithm_t psa_enrollment_alg; -#endif /* MBEDTLS_PSA_CRYPTO_C */ psa_status_t status; - status = psa_get_key_attributes(ctx->priv_id, &key_attr); - if (status != PSA_SUCCESS) { - return PSA_PK_RSA_TO_MBEDTLS_ERR(status); - } - psa_alg = psa_get_key_algorithm(&key_attr); -#if defined(MBEDTLS_PSA_CRYPTO_C) - psa_enrollment_alg = psa_get_key_enrollment_algorithm(&key_attr); -#endif /* MBEDTLS_PSA_CRYPTO_C */ - psa_reset_key_attributes(&key_attr); - - /* Since we're PK type is MBEDTLS_PK_RSASSA_PSS at least one between - * alg and enrollment alg should be of type RSA_PSS. */ - if (PSA_ALG_IS_RSA_PSS(psa_alg)) { - sign_alg = psa_alg; - } -#if defined(MBEDTLS_PSA_CRYPTO_C) - else if (PSA_ALG_IS_RSA_PSS(psa_enrollment_alg)) { - sign_alg = psa_enrollment_alg; - } -#endif /* MBEDTLS_PSA_CRYPTO_C */ - else { - /* The opaque key has no RSA PSS algorithm associated. */ - return MBEDTLS_ERR_PK_BAD_INPUT_DATA; - } - /* Adjust the hashing algorithm. */ - sign_alg = (sign_alg & ~PSA_ALG_HASH_MASK) | PSA_ALG_GET_HASH(psa_md_alg); - - status = psa_sign_hash(ctx->priv_id, sign_alg, + /* PSA_ALG_RSA_PSS() behaves the same as PSA_ALG_RSA_PSS_ANY_SALT() when + * performing a signature, but they are encoded differently. Instead of + * extracting the proper one from the wrapped key policy, just try both. */ + status = psa_sign_hash(ctx->priv_id, PSA_ALG_RSA_PSS(psa_md_alg), hash, hash_len, sig, sig_size, sig_len); + if (status == PSA_ERROR_NOT_PERMITTED) { + status = psa_sign_hash(ctx->priv_id, PSA_ALG_RSA_PSS_ANY_SALT(psa_md_alg), + hash, hash_len, + sig, sig_size, sig_len); + } return PSA_PK_RSA_TO_MBEDTLS_ERR(status); } diff --git a/thirdparty/mbedtls/library/platform_util.c b/thirdparty/mbedtls/library/platform_util.c index 0741bf575e..19ef07aead 100644 --- a/thirdparty/mbedtls/library/platform_util.c +++ b/thirdparty/mbedtls/library/platform_util.c @@ -149,7 +149,7 @@ void mbedtls_zeroize_and_free(void *buf, size_t len) #include <time.h> #if !defined(_WIN32) && (defined(unix) || \ defined(__unix) || defined(__unix__) || (defined(__APPLE__) && \ - defined(__MACH__)) || defined__midipix__) + defined(__MACH__)) || defined(__midipix__)) #include <unistd.h> #endif /* !_WIN32 && (unix || __unix || __unix__ || * (__APPLE__ && __MACH__) || __midipix__) */ diff --git a/thirdparty/mbedtls/library/psa_crypto_core.h b/thirdparty/mbedtls/library/psa_crypto_core.h index c059162efe..21e7559f01 100644 --- a/thirdparty/mbedtls/library/psa_crypto_core.h +++ b/thirdparty/mbedtls/library/psa_crypto_core.h @@ -59,6 +59,8 @@ typedef enum { * and metadata for one key. */ typedef struct { + /* This field is accessed in a lot of places. Putting it first + * reduces the code size. */ psa_key_attributes_t attr; /* @@ -78,35 +80,77 @@ typedef struct { * slots that are in a suitable state for the function. * For example, psa_get_and_lock_key_slot_in_memory, which finds a slot * containing a given key ID, will only check slots whose state variable is - * PSA_SLOT_FULL. */ + * PSA_SLOT_FULL. + */ psa_key_slot_state_t state; - /* - * Number of functions registered as reading the material in the key slot. - * - * Library functions must not write directly to registered_readers - * - * A function must call psa_register_read(slot) before reading the current - * contents of the slot for an operation. - * They then must call psa_unregister_read(slot) once they have finished - * reading the current contents of the slot. If the key slot mutex is not - * held (when mutexes are enabled), this call must be done via a call to - * psa_unregister_read_under_mutex(slot). - * A function must call psa_key_slot_has_readers(slot) to check if - * the slot is in use for reading. +#if defined(MBEDTLS_PSA_KEY_STORE_DYNAMIC) + /* The index of the slice containing this slot. + * This field must be filled if the slot contains a key + * (including keys being created or destroyed), and can be either + * filled or 0 when the slot is free. * - * This counter is used to prevent resetting the key slot while the library - * may access it. For example, such control is needed in the following - * scenarios: - * . In case of key slot starvation, all key slots contain the description - * of a key, and the library asks for the description of a persistent - * key not present in the key slots, the key slots currently accessed by - * the library cannot be reclaimed to free a key slot to load the - * persistent key. - * . In case of a multi-threaded application where one thread asks to close - * or purge or destroy a key while it is in use by the library through - * another thread. */ - size_t registered_readers; + * In most cases, the slice index can be deduced from the key identifer. + * We keep it in a separate field for robustness (it reduces the chance + * that a coding mistake in the key store will result in accessing the + * wrong slice), and also so that it's available even on code paths + * during creation or destruction where the key identifier might not be + * filled in. + * */ + uint8_t slice_index; +#endif /* MBEDTLS_PSA_KEY_STORE_DYNAMIC */ + + union { + struct { + /* The index of the next slot in the free list for this + * slice, relative * to the next array element. + * + * That is, 0 means the next slot, 1 means the next slot + * but one, etc. -1 would mean the slot itself. -2 means + * the previous slot, etc. + * + * If this is beyond the array length, the free list ends with the + * current element. + * + * The reason for this strange encoding is that 0 means the next + * element. This way, when we allocate a slice and initialize it + * to all-zero, the slice is ready for use, with a free list that + * consists of all the slots in order. + */ + int32_t next_free_relative_to_next; + } free; + + struct { + /* + * Number of functions registered as reading the material in the key slot. + * + * Library functions must not write directly to registered_readers + * + * A function must call psa_register_read(slot) before reading + * the current contents of the slot for an operation. + * They then must call psa_unregister_read(slot) once they have + * finished reading the current contents of the slot. If the key + * slot mutex is not held (when mutexes are enabled), this call + * must be done via a call to + * psa_unregister_read_under_mutex(slot). + * A function must call psa_key_slot_has_readers(slot) to check if + * the slot is in use for reading. + * + * This counter is used to prevent resetting the key slot while + * the library may access it. For example, such control is needed + * in the following scenarios: + * . In case of key slot starvation, all key slots contain the + * description of a key, and the library asks for the + * description of a persistent key not present in the + * key slots, the key slots currently accessed by the + * library cannot be reclaimed to free a key slot to load + * the persistent key. + * . In case of a multi-threaded application where one thread + * asks to close or purge or destroy a key while it is in use + * by the library through another thread. */ + size_t registered_readers; + } occupied; + } var; /* Dynamically allocated key data buffer. * Format as specified in psa_export_key(). */ @@ -169,7 +213,7 @@ typedef struct { */ static inline int psa_key_slot_has_readers(const psa_key_slot_t *slot) { - return slot->registered_readers > 0; + return slot->var.occupied.registered_readers > 0; } #if defined(MBEDTLS_PSA_CRYPTO_SE_C) @@ -343,19 +387,18 @@ psa_status_t psa_export_public_key_internal( const uint8_t *key_buffer, size_t key_buffer_size, uint8_t *data, size_t data_size, size_t *data_length); -/** Whether a key production parameters structure is the default. +/** Whether a key custom production parameters structure is the default. * - * Calls to a key generation driver with non-default production parameters + * Calls to a key generation driver with non-default custom production parameters * require a driver supporting custom production parameters. * - * \param[in] params The key production parameters to check. - * \param params_data_length Size of `params->data` in bytes. + * \param[in] custom The key custom production parameters to check. + * \param custom_data_length Size of the associated variable-length data + * in bytes. */ -#ifndef __cplusplus -int psa_key_production_parameters_are_default( - const psa_key_production_parameters_t *params, - size_t params_data_length); -#endif +int psa_custom_key_parameters_are_default( + const psa_custom_key_parameters_t *custom, + size_t custom_data_length); /** * \brief Generate a key. @@ -364,9 +407,9 @@ int psa_key_production_parameters_are_default( * entry point. * * \param[in] attributes The attributes for the key to generate. - * \param[in] params The production parameters from - * psa_generate_key_ext(). - * \param params_data_length The size of `params->data` in bytes. + * \param[in] custom Custom parameters for the key generation. + * \param[in] custom_data Variable-length data associated with \c custom. + * \param custom_data_length Length of `custom_data` in bytes. * \param[out] key_buffer Buffer where the key data is to be written. * \param[in] key_buffer_size Size of \p key_buffer in bytes. * \param[out] key_buffer_length On success, the number of bytes written in @@ -380,14 +423,13 @@ int psa_key_production_parameters_are_default( * \retval #PSA_ERROR_BUFFER_TOO_SMALL * The size of \p key_buffer is too small. */ -#ifndef __cplusplus psa_status_t psa_generate_key_internal(const psa_key_attributes_t *attributes, - const psa_key_production_parameters_t *params, - size_t params_data_length, + const psa_custom_key_parameters_t *custom, + const uint8_t *custom_data, + size_t custom_data_length, uint8_t *key_buffer, size_t key_buffer_size, size_t *key_buffer_length); -#endif /** Sign a message with a private key. For hash-and-sign algorithms, * this includes the hashing step. diff --git a/thirdparty/mbedtls/library/psa_crypto_driver_wrappers.h b/thirdparty/mbedtls/library/psa_crypto_driver_wrappers.h index 6919971aca..b901557208 100644 --- a/thirdparty/mbedtls/library/psa_crypto_driver_wrappers.h +++ b/thirdparty/mbedtls/library/psa_crypto_driver_wrappers.h @@ -728,10 +728,10 @@ static inline psa_status_t psa_driver_wrapper_get_key_buffer_size_from_key_data( } } -#ifndef __cplusplus static inline psa_status_t psa_driver_wrapper_generate_key( const psa_key_attributes_t *attributes, - const psa_key_production_parameters_t *params, size_t params_data_length, + const psa_custom_key_parameters_t *custom, + const uint8_t *custom_data, size_t custom_data_length, uint8_t *key_buffer, size_t key_buffer_size, size_t *key_buffer_length ) { psa_status_t status = PSA_ERROR_CORRUPTION_DETECTED; @@ -740,7 +740,7 @@ static inline psa_status_t psa_driver_wrapper_generate_key( #if defined(PSA_WANT_KEY_TYPE_RSA_KEY_PAIR_GENERATE) int is_default_production = - psa_key_production_parameters_are_default(params, params_data_length); + psa_custom_key_parameters_are_default(custom, custom_data_length); if( location != PSA_KEY_LOCATION_LOCAL_STORAGE && !is_default_production ) { /* We don't support passing custom production parameters @@ -811,7 +811,7 @@ static inline psa_status_t psa_driver_wrapper_generate_key( /* Software fallback */ status = psa_generate_key_internal( - attributes, params, params_data_length, + attributes, custom, custom_data, custom_data_length, key_buffer, key_buffer_size, key_buffer_length ); break; @@ -833,7 +833,6 @@ static inline psa_status_t psa_driver_wrapper_generate_key( return( status ); } -#endif static inline psa_status_t psa_driver_wrapper_import_key( const psa_key_attributes_t *attributes, diff --git a/thirdparty/mbedtls/library/psa_crypto_random_impl.h b/thirdparty/mbedtls/library/psa_crypto_random_impl.h index 533fb2e940..5b5163111b 100644 --- a/thirdparty/mbedtls/library/psa_crypto_random_impl.h +++ b/thirdparty/mbedtls/library/psa_crypto_random_impl.h @@ -21,13 +21,10 @@ typedef mbedtls_psa_external_random_context_t mbedtls_psa_random_context_t; #include "mbedtls/entropy.h" /* Choose a DRBG based on configuration and availability */ -#if defined(MBEDTLS_PSA_HMAC_DRBG_MD_TYPE) - -#include "mbedtls/hmac_drbg.h" - -#elif defined(MBEDTLS_CTR_DRBG_C) +#if defined(MBEDTLS_CTR_DRBG_C) #include "mbedtls/ctr_drbg.h" +#undef MBEDTLS_PSA_HMAC_DRBG_MD_TYPE #elif defined(MBEDTLS_HMAC_DRBG_C) @@ -49,17 +46,11 @@ typedef mbedtls_psa_external_random_context_t mbedtls_psa_random_context_t; #error "No hash algorithm available for HMAC_DBRG." #endif -#else /* !MBEDTLS_PSA_HMAC_DRBG_MD_TYPE && !MBEDTLS_CTR_DRBG_C && !MBEDTLS_HMAC_DRBG_C*/ +#else /* !MBEDTLS_CTR_DRBG_C && !MBEDTLS_HMAC_DRBG_C*/ #error "No DRBG module available for the psa_crypto module." -#endif /* !MBEDTLS_PSA_HMAC_DRBG_MD_TYPE && !MBEDTLS_CTR_DRBG_C && !MBEDTLS_HMAC_DRBG_C*/ - -#if defined(MBEDTLS_CTR_DRBG_C) -#include "mbedtls/ctr_drbg.h" -#elif defined(MBEDTLS_HMAC_DRBG_C) -#include "mbedtls/hmac_drbg.h" -#endif /* !MBEDTLS_CTR_DRBG_C && !MBEDTLS_HMAC_DRBG_C */ +#endif /* !MBEDTLS_CTR_DRBG_C && !MBEDTLS_HMAC_DRBG_C*/ /* The maximum number of bytes that mbedtls_psa_get_random() is expected to return. */ #if defined(MBEDTLS_CTR_DRBG_C) diff --git a/thirdparty/mbedtls/library/psa_crypto_rsa.h b/thirdparty/mbedtls/library/psa_crypto_rsa.h index 6d695ddf50..1a780006a9 100644 --- a/thirdparty/mbedtls/library/psa_crypto_rsa.h +++ b/thirdparty/mbedtls/library/psa_crypto_rsa.h @@ -105,17 +105,11 @@ psa_status_t mbedtls_psa_rsa_export_public_key( /** * \brief Generate an RSA key. * - * \note The signature of the function is that of a PSA driver generate_key - * entry point. - * * \param[in] attributes The attributes for the RSA key to generate. - * \param[in] params Production parameters for the key - * generation. This function only uses - * `params->data`, - * which contains the public exponent. + * \param[in] custom_data The public exponent to use. * This can be a null pointer if * \c params_data_length is 0. - * \param params_data_length Length of `params->data` in bytes. + * \param custom_data_length Length of \p custom_data in bytes. * This can be 0, in which case the * public exponent will be 65537. * \param[out] key_buffer Buffer where the key data is to be written. @@ -130,12 +124,10 @@ psa_status_t mbedtls_psa_rsa_export_public_key( * \retval #PSA_ERROR_BUFFER_TOO_SMALL * The size of \p key_buffer is too small. */ -#ifndef __cplusplus psa_status_t mbedtls_psa_rsa_generate_key( const psa_key_attributes_t *attributes, - const psa_key_production_parameters_t *params, size_t params_data_length, + const uint8_t *custom_data, size_t custom_data_length, uint8_t *key_buffer, size_t key_buffer_size, size_t *key_buffer_length); -#endif /** Sign an already-calculated hash with an RSA private key. * diff --git a/thirdparty/mbedtls/library/psa_crypto_slot_management.h b/thirdparty/mbedtls/library/psa_crypto_slot_management.h index bcfc9d8adc..af1208e3ae 100644 --- a/thirdparty/mbedtls/library/psa_crypto_slot_management.h +++ b/thirdparty/mbedtls/library/psa_crypto_slot_management.h @@ -15,20 +15,26 @@ /** Range of volatile key identifiers. * - * The last #MBEDTLS_PSA_KEY_SLOT_COUNT identifiers of the implementation + * The first #MBEDTLS_PSA_KEY_SLOT_COUNT identifiers of the implementation * range of key identifiers are reserved for volatile key identifiers. - * A volatile key identifier is equal to #PSA_KEY_ID_VOLATILE_MIN plus the - * index of the key slot containing the volatile key definition. + * + * If \c id is a a volatile key identifier, #PSA_KEY_ID_VOLATILE_MIN - \c id + * indicates the key slot containing the volatile key definition. See + * psa_crypto_slot_management.c for details. */ /** The minimum value for a volatile key identifier. */ -#define PSA_KEY_ID_VOLATILE_MIN (PSA_KEY_ID_VENDOR_MAX - \ - MBEDTLS_PSA_KEY_SLOT_COUNT + 1) +#define PSA_KEY_ID_VOLATILE_MIN PSA_KEY_ID_VENDOR_MIN /** The maximum value for a volatile key identifier. */ -#define PSA_KEY_ID_VOLATILE_MAX PSA_KEY_ID_VENDOR_MAX +#if defined(MBEDTLS_PSA_KEY_STORE_DYNAMIC) +#define PSA_KEY_ID_VOLATILE_MAX (MBEDTLS_PSA_KEY_ID_BUILTIN_MIN - 1) +#else /* MBEDTLS_PSA_KEY_STORE_DYNAMIC */ +#define PSA_KEY_ID_VOLATILE_MAX \ + (PSA_KEY_ID_VOLATILE_MIN + MBEDTLS_PSA_KEY_SLOT_COUNT - 1) +#endif /* MBEDTLS_PSA_KEY_STORE_DYNAMIC */ /** Test whether a key identifier is a volatile key identifier. * @@ -58,6 +64,9 @@ static inline int psa_key_id_is_volatile(psa_key_id_t key_id) * It is the responsibility of the caller to call psa_unregister_read(slot) * when they have finished reading the contents of the slot. * + * On failure, `*p_slot` is set to NULL. This ensures that it is always valid + * to call psa_unregister_read on the returned slot. + * * \param key Key identifier to query. * \param[out] p_slot On success, `*p_slot` contains a pointer to the * key slot containing the description of the key @@ -91,6 +100,24 @@ psa_status_t psa_get_and_lock_key_slot(mbedtls_svc_key_id_t key, */ psa_status_t psa_initialize_key_slots(void); +#if defined(MBEDTLS_TEST_HOOKS) && defined(MBEDTLS_PSA_KEY_STORE_DYNAMIC) +/* Allow test code to customize the key slice length. We use this in tests + * that exhaust the key store to reach a full key store in reasonable time + * and memory. + * + * The length of each slice must be between 1 and + * (1 << KEY_ID_SLOT_INDEX_WIDTH) inclusive. + * + * The length for a given slice index must not change while + * the key store is initialized. + */ +extern size_t (*mbedtls_test_hook_psa_volatile_key_slice_length)( + size_t slice_idx); + +/* The number of volatile key slices. */ +size_t psa_key_slot_volatile_slice_count(void); +#endif + /** Delete all data from key slots in memory. * This function is not thread safe, it wipes every key slot regardless of * state and reader count. It should only be called when no slot is in use. @@ -110,13 +137,22 @@ void psa_wipe_all_key_slots(void); * If multi-threading is enabled, the caller must hold the * global key slot mutex. * - * \param[out] volatile_key_id On success, volatile key identifier - * associated to the returned slot. + * \param[out] volatile_key_id - If null, reserve a cache slot for + * a persistent or built-in key. + * - If non-null, allocate a slot for + * a volatile key. On success, + * \p *volatile_key_id is the + * identifier corresponding to the + * returned slot. It is the caller's + * responsibility to set this key identifier + * in the attributes. * \param[out] p_slot On success, a pointer to the slot. * * \retval #PSA_SUCCESS \emptydescription * \retval #PSA_ERROR_INSUFFICIENT_MEMORY * There were no free key slots. + * When #MBEDTLS_PSA_KEY_STORE_DYNAMIC is enabled, there was not + * enough memory to allocate more slots. * \retval #PSA_ERROR_BAD_STATE \emptydescription * \retval #PSA_ERROR_CORRUPTION_DETECTED * This function attempted to operate on a key slot which was in an @@ -125,6 +161,29 @@ void psa_wipe_all_key_slots(void); psa_status_t psa_reserve_free_key_slot(psa_key_id_t *volatile_key_id, psa_key_slot_t **p_slot); +#if defined(MBEDTLS_PSA_KEY_STORE_DYNAMIC) +/** Return a key slot to the free list. + * + * Call this function when a slot obtained from psa_reserve_free_key_slot() + * is no longer in use. + * + * If multi-threading is enabled, the caller must hold the + * global key slot mutex. + * + * \param slice_idx The slice containing the slot. + * This is `slot->slice_index` when the slot + * is obtained from psa_reserve_free_key_slot(). + * \param slot The key slot. + * + * \retval #PSA_SUCCESS \emptydescription + * \retval #PSA_ERROR_CORRUPTION_DETECTED + * This function attempted to operate on a key slot which was in an + * unexpected state. + */ +psa_status_t psa_free_key_slot(size_t slice_idx, + psa_key_slot_t *slot); +#endif /* MBEDTLS_PSA_KEY_STORE_DYNAMIC */ + /** Change the state of a key slot. * * This function changes the state of the key slot from expected_state to @@ -171,10 +230,10 @@ static inline psa_status_t psa_key_slot_state_transition( static inline psa_status_t psa_register_read(psa_key_slot_t *slot) { if ((slot->state != PSA_SLOT_FULL) || - (slot->registered_readers >= SIZE_MAX)) { + (slot->var.occupied.registered_readers >= SIZE_MAX)) { return PSA_ERROR_CORRUPTION_DETECTED; } - slot->registered_readers++; + slot->var.occupied.registered_readers++; return PSA_SUCCESS; } diff --git a/thirdparty/mbedtls/library/rsa.c b/thirdparty/mbedtls/library/rsa.c index 7eb4a259ea..557faaf363 100644 --- a/thirdparty/mbedtls/library/rsa.c +++ b/thirdparty/mbedtls/library/rsa.c @@ -29,6 +29,7 @@ #include "mbedtls/rsa.h" #include "bignum_core.h" +#include "bignum_internal.h" #include "rsa_alt_helpers.h" #include "rsa_internal.h" #include "mbedtls/oid.h" @@ -1259,7 +1260,7 @@ int mbedtls_rsa_public(mbedtls_rsa_context *ctx, } olen = ctx->len; - MBEDTLS_MPI_CHK(mbedtls_mpi_exp_mod(&T, &T, &ctx->E, &ctx->N, &ctx->RN)); + MBEDTLS_MPI_CHK(mbedtls_mpi_exp_mod_unsafe(&T, &T, &ctx->E, &ctx->N, &ctx->RN)); MBEDTLS_MPI_CHK(mbedtls_mpi_write_binary(&T, output, olen)); cleanup: diff --git a/thirdparty/mbedtls/library/sha256.c b/thirdparty/mbedtls/library/sha256.c index 87889817a4..159acccaeb 100644 --- a/thirdparty/mbedtls/library/sha256.c +++ b/thirdparty/mbedtls/library/sha256.c @@ -44,7 +44,9 @@ #endif /* defined(__clang__) && (__clang_major__ >= 4) */ /* Ensure that SIG_SETMASK is defined when -std=c99 is used. */ +#if !defined(_GNU_SOURCE) #define _GNU_SOURCE +#endif #include "common.h" @@ -150,7 +152,9 @@ static int mbedtls_a64_crypto_sha256_determine_support(void) return 1; } #elif defined(MBEDTLS_PLATFORM_IS_WINDOWS_ON_ARM64) +#ifndef WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN +#endif #include <Windows.h> #include <processthreadsapi.h> diff --git a/thirdparty/mbedtls/library/ssl_cookie.c b/thirdparty/mbedtls/library/ssl_cookie.c index 2772cac4be..acc9e8c080 100644 --- a/thirdparty/mbedtls/library/ssl_cookie.c +++ b/thirdparty/mbedtls/library/ssl_cookie.c @@ -84,6 +84,10 @@ void mbedtls_ssl_cookie_set_timeout(mbedtls_ssl_cookie_ctx *ctx, unsigned long d void mbedtls_ssl_cookie_free(mbedtls_ssl_cookie_ctx *ctx) { + if (ctx == NULL) { + return; + } + #if defined(MBEDTLS_USE_PSA_CRYPTO) psa_destroy_key(ctx->psa_hmac_key); #else diff --git a/thirdparty/mbedtls/library/ssl_debug_helpers_generated.c b/thirdparty/mbedtls/library/ssl_debug_helpers_generated.c index f8b4448c86..734c417b8b 100644 --- a/thirdparty/mbedtls/library/ssl_debug_helpers_generated.c +++ b/thirdparty/mbedtls/library/ssl_debug_helpers_generated.c @@ -60,7 +60,7 @@ const char *mbedtls_ssl_named_group_to_str( uint16_t in ) return "ffdhe8192"; }; - return "UNKOWN"; + return "UNKNOWN"; } const char *mbedtls_ssl_sig_alg_to_str( uint16_t in ) { diff --git a/thirdparty/mbedtls/library/ssl_misc.h b/thirdparty/mbedtls/library/ssl_misc.h index a8807f67c6..98668798a8 100644 --- a/thirdparty/mbedtls/library/ssl_misc.h +++ b/thirdparty/mbedtls/library/ssl_misc.h @@ -1507,7 +1507,7 @@ int mbedtls_ssl_psk_derive_premaster(mbedtls_ssl_context *ssl, #endif /* MBEDTLS_KEY_EXCHANGE_SOME_PSK_ENABLED */ #if defined(MBEDTLS_SSL_HANDSHAKE_WITH_PSK_ENABLED) -#if defined(MBEDTLS_SSL_CLI_C) +#if defined(MBEDTLS_SSL_CLI_C) || defined(MBEDTLS_SSL_SRV_C) MBEDTLS_CHECK_RETURN_CRITICAL int mbedtls_ssl_conf_has_static_psk(mbedtls_ssl_config const *conf); #endif @@ -1674,18 +1674,53 @@ static inline mbedtls_x509_crt *mbedtls_ssl_own_cert(mbedtls_ssl_context *ssl) } /* - * Check usage of a certificate wrt extensions: - * keyUsage, extendedKeyUsage (later), and nSCertType (later). + * Verify a certificate. + * + * [in/out] ssl: misc. things read + * ssl->session_negotiate->verify_result updated + * [in] authmode: one of MBEDTLS_SSL_VERIFY_{NONE,OPTIONAL,REQUIRED} + * [in] chain: the certificate chain to verify (ie the peer's chain) + * [in] ciphersuite_info: For TLS 1.2, this session's ciphersuite; + * for TLS 1.3, may be left NULL. + * [in] rs_ctx: restart context if restartable ECC is in use; + * leave NULL for no restartable behaviour. + * + * Return: + * - 0 if the handshake should continue. Depending on the + * authmode it means: + * - REQUIRED: the certificate was found to be valid, trusted & acceptable. + * ssl->session_negotiate->verify_result is 0. + * - OPTIONAL: the certificate may or may not be acceptable, but + * ssl->session_negotiate->verify_result was updated with the result. + * - NONE: the certificate wasn't even checked. + * - MBEDTLS_ERR_X509_CERT_VERIFY_FAILED or MBEDTLS_ERR_SSL_BAD_CERTIFICATE if + * the certificate was found to be invalid/untrusted/unacceptable and the + * handshake should be aborted (can only happen with REQUIRED). + * - another error code if another error happened (out-of-memory, etc.) + */ +MBEDTLS_CHECK_RETURN_CRITICAL +int mbedtls_ssl_verify_certificate(mbedtls_ssl_context *ssl, + int authmode, + mbedtls_x509_crt *chain, + const mbedtls_ssl_ciphersuite_t *ciphersuite_info, + void *rs_ctx); + +/* + * Check usage of a certificate wrt usage extensions: + * keyUsage and extendedKeyUsage. + * (Note: nSCertType is deprecated and not standard, we don't check it.) * - * Warning: cert_endpoint is the endpoint of the cert (ie, of our peer when we - * check a cert we received from them)! + * Note: if tls_version is 1.3, ciphersuite is ignored and can be NULL. + * + * Note: recv_endpoint is the receiver's endpoint. * * Return 0 if everything is OK, -1 if not. */ MBEDTLS_CHECK_RETURN_CRITICAL int mbedtls_ssl_check_cert_usage(const mbedtls_x509_crt *cert, const mbedtls_ssl_ciphersuite_t *ciphersuite, - int cert_endpoint, + int recv_endpoint, + mbedtls_ssl_protocol_version tls_version, uint32_t *flags); #endif /* MBEDTLS_X509_CRT_PARSE_C */ @@ -1891,6 +1926,26 @@ static inline int mbedtls_ssl_conf_is_hybrid_tls12_tls13(const mbedtls_ssl_confi #endif /* MBEDTLS_SSL_PROTO_TLS1_2 && MBEDTLS_SSL_PROTO_TLS1_3 */ #if defined(MBEDTLS_SSL_PROTO_TLS1_3) + +/** \brief Initialize the PSA crypto subsystem if necessary. + * + * Call this function before doing any cryptography in a TLS 1.3 handshake. + * + * This is necessary in Mbed TLS 3.x for backward compatibility. + * Up to Mbed TLS 3.5, in the default configuration, you could perform + * a TLS connection with default parameters without having called + * psa_crypto_init(), since the TLS layer only supported TLS 1.2 and + * did not use PSA crypto. (TLS 1.2 only uses PSA crypto if + * MBEDTLS_USE_PSA_CRYPTO is enabled, which is not the case in the default + * configuration.) Starting with Mbed TLS 3.6.0, TLS 1.3 is enabled + * by default, and the TLS 1.3 layer uses PSA crypto. This means that + * applications that are not otherwise using PSA crypto and that worked + * with Mbed TLS 3.5 started failing in TLS 3.6.0 if they connected to + * a peer that supports TLS 1.3. See + * https://github.com/Mbed-TLS/mbedtls/issues/9072 + */ +int mbedtls_ssl_tls13_crypto_init(mbedtls_ssl_context *ssl); + extern const uint8_t mbedtls_ssl_tls13_hello_retry_request_magic[ MBEDTLS_SERVER_HELLO_RANDOM_LEN]; MBEDTLS_CHECK_RETURN_CRITICAL @@ -2914,8 +2969,37 @@ static inline void mbedtls_ssl_tls13_session_clear_ticket_flags( { session->ticket_flags &= ~(flags & MBEDTLS_SSL_TLS1_3_TICKET_FLAGS_MASK); } + #endif /* MBEDTLS_SSL_PROTO_TLS1_3 && MBEDTLS_SSL_SESSION_TICKETS */ +#if defined(MBEDTLS_SSL_SESSION_TICKETS) && defined(MBEDTLS_SSL_CLI_C) +#define MBEDTLS_SSL_SESSION_TICKETS_TLS1_2_BIT 0 +#define MBEDTLS_SSL_SESSION_TICKETS_TLS1_3_BIT 1 + +#define MBEDTLS_SSL_SESSION_TICKETS_TLS1_2_MASK \ + (1 << MBEDTLS_SSL_SESSION_TICKETS_TLS1_2_BIT) +#define MBEDTLS_SSL_SESSION_TICKETS_TLS1_3_MASK \ + (1 << MBEDTLS_SSL_SESSION_TICKETS_TLS1_3_BIT) + +static inline int mbedtls_ssl_conf_get_session_tickets( + const mbedtls_ssl_config *conf) +{ + return conf->session_tickets & MBEDTLS_SSL_SESSION_TICKETS_TLS1_2_MASK ? + MBEDTLS_SSL_SESSION_TICKETS_ENABLED : + MBEDTLS_SSL_SESSION_TICKETS_DISABLED; +} + +#if defined(MBEDTLS_SSL_PROTO_TLS1_3) +static inline int mbedtls_ssl_conf_is_signal_new_session_tickets_enabled( + const mbedtls_ssl_config *conf) +{ + return conf->session_tickets & MBEDTLS_SSL_SESSION_TICKETS_TLS1_3_MASK ? + MBEDTLS_SSL_TLS1_3_SIGNAL_NEW_SESSION_TICKETS_ENABLED : + MBEDTLS_SSL_TLS1_3_SIGNAL_NEW_SESSION_TICKETS_DISABLED; +} +#endif /* MBEDTLS_SSL_PROTO_TLS1_3 */ +#endif /* MBEDTLS_SSL_SESSION_TICKETS && MBEDTLS_SSL_CLI_C */ + #if defined(MBEDTLS_SSL_CLI_C) && defined(MBEDTLS_SSL_PROTO_TLS1_3) int mbedtls_ssl_tls13_finalize_client_hello(mbedtls_ssl_context *ssl); #endif diff --git a/thirdparty/mbedtls/library/ssl_msg.c b/thirdparty/mbedtls/library/ssl_msg.c index b07cd96f1b..ef722d7bdc 100644 --- a/thirdparty/mbedtls/library/ssl_msg.c +++ b/thirdparty/mbedtls/library/ssl_msg.c @@ -5570,9 +5570,9 @@ static int ssl_check_ctr_renegotiate(mbedtls_ssl_context *ssl) #if defined(MBEDTLS_SSL_PROTO_TLS1_3) -#if defined(MBEDTLS_SSL_SESSION_TICKETS) && defined(MBEDTLS_SSL_CLI_C) +#if defined(MBEDTLS_SSL_CLI_C) MBEDTLS_CHECK_RETURN_CRITICAL -static int ssl_tls13_check_new_session_ticket(mbedtls_ssl_context *ssl) +static int ssl_tls13_is_new_session_ticket(mbedtls_ssl_context *ssl) { if ((ssl->in_hslen == mbedtls_ssl_hs_hdr_len(ssl)) || @@ -5580,15 +5580,9 @@ static int ssl_tls13_check_new_session_ticket(mbedtls_ssl_context *ssl) return 0; } - ssl->keep_current_message = 1; - - MBEDTLS_SSL_DEBUG_MSG(3, ("NewSessionTicket received")); - mbedtls_ssl_handshake_set_state(ssl, - MBEDTLS_SSL_TLS1_3_NEW_SESSION_TICKET); - - return MBEDTLS_ERR_SSL_WANT_READ; + return 1; } -#endif /* MBEDTLS_SSL_SESSION_TICKETS && MBEDTLS_SSL_CLI_C */ +#endif /* MBEDTLS_SSL_CLI_C */ MBEDTLS_CHECK_RETURN_CRITICAL static int ssl_tls13_handle_hs_message_post_handshake(mbedtls_ssl_context *ssl) @@ -5596,14 +5590,29 @@ static int ssl_tls13_handle_hs_message_post_handshake(mbedtls_ssl_context *ssl) MBEDTLS_SSL_DEBUG_MSG(3, ("received post-handshake message")); -#if defined(MBEDTLS_SSL_SESSION_TICKETS) && defined(MBEDTLS_SSL_CLI_C) +#if defined(MBEDTLS_SSL_CLI_C) if (ssl->conf->endpoint == MBEDTLS_SSL_IS_CLIENT) { - int ret = ssl_tls13_check_new_session_ticket(ssl); - if (ret != 0) { - return ret; + if (ssl_tls13_is_new_session_ticket(ssl)) { +#if defined(MBEDTLS_SSL_SESSION_TICKETS) + MBEDTLS_SSL_DEBUG_MSG(3, ("NewSessionTicket received")); + if (mbedtls_ssl_conf_is_signal_new_session_tickets_enabled(ssl->conf) == + MBEDTLS_SSL_TLS1_3_SIGNAL_NEW_SESSION_TICKETS_ENABLED) { + ssl->keep_current_message = 1; + + mbedtls_ssl_handshake_set_state(ssl, + MBEDTLS_SSL_TLS1_3_NEW_SESSION_TICKET); + return MBEDTLS_ERR_SSL_WANT_READ; + } else { + MBEDTLS_SSL_DEBUG_MSG(3, ("Ignoring NewSessionTicket, handling disabled.")); + return 0; + } +#else + MBEDTLS_SSL_DEBUG_MSG(3, ("Ignoring NewSessionTicket, not supported.")); + return 0; +#endif } } -#endif /* MBEDTLS_SSL_SESSION_TICKETS && MBEDTLS_SSL_CLI_C */ +#endif /* MBEDTLS_SSL_CLI_C */ /* Fail in all other cases. */ return MBEDTLS_ERR_SSL_UNEXPECTED_MESSAGE; diff --git a/thirdparty/mbedtls/library/ssl_ticket.c b/thirdparty/mbedtls/library/ssl_ticket.c index 6a31b0bee6..bfb656cf62 100644 --- a/thirdparty/mbedtls/library/ssl_ticket.c +++ b/thirdparty/mbedtls/library/ssl_ticket.c @@ -534,6 +534,10 @@ cleanup: */ void mbedtls_ssl_ticket_free(mbedtls_ssl_ticket_context *ctx) { + if (ctx == NULL) { + return; + } + #if defined(MBEDTLS_USE_PSA_CRYPTO) psa_destroy_key(ctx->keys[0].key); psa_destroy_key(ctx->keys[1].key); diff --git a/thirdparty/mbedtls/library/ssl_tls.c b/thirdparty/mbedtls/library/ssl_tls.c index c5e06491c1..c773365bf6 100644 --- a/thirdparty/mbedtls/library/ssl_tls.c +++ b/thirdparty/mbedtls/library/ssl_tls.c @@ -132,7 +132,7 @@ int mbedtls_ssl_set_cid(mbedtls_ssl_context *ssl, int mbedtls_ssl_get_own_cid(mbedtls_ssl_context *ssl, int *enabled, - unsigned char own_cid[MBEDTLS_SSL_CID_OUT_LEN_MAX], + unsigned char own_cid[MBEDTLS_SSL_CID_IN_LEN_MAX], size_t *own_cid_len) { *enabled = MBEDTLS_SSL_CID_DISABLED; @@ -1354,29 +1354,6 @@ static int ssl_conf_check(const mbedtls_ssl_context *ssl) return ret; } -#if defined(MBEDTLS_SSL_PROTO_TLS1_3) - /* RFC 8446 section 4.4.3 - * - * If the verification fails, the receiver MUST terminate the handshake with - * a "decrypt_error" alert. - * - * If the client is configured as TLS 1.3 only with optional verify, return - * bad config. - * - */ - if (mbedtls_ssl_conf_tls13_is_ephemeral_enabled( - (mbedtls_ssl_context *) ssl) && - ssl->conf->endpoint == MBEDTLS_SSL_IS_CLIENT && - ssl->conf->max_tls_version == MBEDTLS_SSL_VERSION_TLS1_3 && - ssl->conf->min_tls_version == MBEDTLS_SSL_VERSION_TLS1_3 && - ssl->conf->authmode == MBEDTLS_SSL_VERIFY_OPTIONAL) { - MBEDTLS_SSL_DEBUG_MSG( - 1, ("Optional verify auth mode " - "is not available for TLS 1.3 client")); - return MBEDTLS_ERR_SSL_BAD_CONFIG; - } -#endif /* MBEDTLS_SSL_PROTO_TLS1_3 */ - if (ssl->conf->f_rng == NULL) { MBEDTLS_SSL_DEBUG_MSG(1, ("no RNG provided")); return MBEDTLS_ERR_SSL_NO_RNG; @@ -1760,6 +1737,7 @@ int mbedtls_ssl_set_session(mbedtls_ssl_context *ssl, const mbedtls_ssl_session #if defined(MBEDTLS_SSL_PROTO_TLS1_3) if (session->tls_version == MBEDTLS_SSL_VERSION_TLS1_3) { +#if defined(MBEDTLS_SSL_SESSION_TICKETS) const mbedtls_ssl_ciphersuite_t *ciphersuite_info = mbedtls_ssl_ciphersuite_from_id(session->ciphersuite); @@ -1770,6 +1748,14 @@ int mbedtls_ssl_set_session(mbedtls_ssl_context *ssl, const mbedtls_ssl_session session->ciphersuite)); return MBEDTLS_ERR_SSL_BAD_INPUT_DATA; } +#else + /* + * If session tickets are not enabled, it is not possible to resume a + * TLS 1.3 session, thus do not make any change to the SSL context in + * the first place. + */ + return 0; +#endif } #endif /* MBEDTLS_SSL_PROTO_TLS1_3 */ @@ -2234,6 +2220,7 @@ static void ssl_remove_psk(mbedtls_ssl_context *ssl) mbedtls_zeroize_and_free(ssl->handshake->psk, ssl->handshake->psk_len); ssl->handshake->psk_len = 0; + ssl->handshake->psk = NULL; } #endif /* MBEDTLS_USE_PSA_CRYPTO */ } @@ -2999,11 +2986,24 @@ void mbedtls_ssl_conf_renegotiation_period(mbedtls_ssl_config *conf, #if defined(MBEDTLS_SSL_SESSION_TICKETS) #if defined(MBEDTLS_SSL_CLI_C) + void mbedtls_ssl_conf_session_tickets(mbedtls_ssl_config *conf, int use_tickets) { - conf->session_tickets = use_tickets; + conf->session_tickets &= ~MBEDTLS_SSL_SESSION_TICKETS_TLS1_2_MASK; + conf->session_tickets |= (use_tickets != 0) << + MBEDTLS_SSL_SESSION_TICKETS_TLS1_2_BIT; } -#endif + +#if defined(MBEDTLS_SSL_PROTO_TLS1_3) +void mbedtls_ssl_conf_tls13_enable_signal_new_session_tickets( + mbedtls_ssl_config *conf, int signal_new_session_tickets) +{ + conf->session_tickets &= ~MBEDTLS_SSL_SESSION_TICKETS_TLS1_3_MASK; + conf->session_tickets |= (signal_new_session_tickets != 0) << + MBEDTLS_SSL_SESSION_TICKETS_TLS1_3_BIT; +} +#endif /* MBEDTLS_SSL_PROTO_TLS1_3 */ +#endif /* MBEDTLS_SSL_CLI_C */ #if defined(MBEDTLS_SSL_SRV_C) @@ -4049,7 +4049,7 @@ static int ssl_tls13_session_save(const mbedtls_ssl_session *session, } static int ssl_tls13_session_load(const mbedtls_ssl_session *session, - unsigned char *buf, + const unsigned char *buf, size_t buf_len) { ((void) session); @@ -5868,7 +5868,33 @@ int mbedtls_ssl_config_defaults(mbedtls_ssl_config *conf, if (endpoint == MBEDTLS_SSL_IS_CLIENT) { conf->authmode = MBEDTLS_SSL_VERIFY_REQUIRED; #if defined(MBEDTLS_SSL_SESSION_TICKETS) - conf->session_tickets = MBEDTLS_SSL_SESSION_TICKETS_ENABLED; + mbedtls_ssl_conf_session_tickets(conf, MBEDTLS_SSL_SESSION_TICKETS_ENABLED); +#if defined(MBEDTLS_SSL_PROTO_TLS1_3) + /* Contrary to TLS 1.2 tickets, TLS 1.3 NewSessionTicket message + * handling is disabled by default in Mbed TLS 3.6.x for backward + * compatibility with client applications developed using Mbed TLS 3.5 + * or earlier with the default configuration. + * + * Up to Mbed TLS 3.5, in the default configuration TLS 1.3 was + * disabled, and a Mbed TLS client with the default configuration would + * establish a TLS 1.2 connection with a TLS 1.2 and TLS 1.3 capable + * server. + * + * Starting with Mbed TLS 3.6.0, TLS 1.3 is enabled by default, and thus + * an Mbed TLS client with the default configuration establishes a + * TLS 1.3 connection with a TLS 1.2 and TLS 1.3 capable server. If + * following the handshake the TLS 1.3 server sends NewSessionTicket + * messages and the Mbed TLS client processes them, this results in + * Mbed TLS high level APIs (mbedtls_ssl_read(), + * mbedtls_ssl_handshake(), ...) to eventually return an + * #MBEDTLS_ERR_SSL_RECEIVED_NEW_SESSION_TICKET non fatal error code + * (see the documentation of mbedtls_ssl_read() for more information on + * that error code). Applications unaware of that TLS 1.3 specific non + * fatal error code are then failing. + */ + mbedtls_ssl_conf_tls13_enable_signal_new_session_tickets( + conf, MBEDTLS_SSL_TLS1_3_SIGNAL_NEW_SESSION_TICKETS_DISABLED); +#endif #endif } #endif @@ -6030,6 +6056,10 @@ int mbedtls_ssl_config_defaults(mbedtls_ssl_config *conf, */ void mbedtls_ssl_config_free(mbedtls_ssl_config *conf) { + if (conf == NULL) { + return; + } + #if defined(MBEDTLS_DHM_C) mbedtls_mpi_free(&conf->dhm_P); mbedtls_mpi_free(&conf->dhm_G); @@ -6344,71 +6374,6 @@ const char *mbedtls_ssl_get_curve_name_from_tls_id(uint16_t tls_id) } #endif -#if defined(MBEDTLS_X509_CRT_PARSE_C) -int mbedtls_ssl_check_cert_usage(const mbedtls_x509_crt *cert, - const mbedtls_ssl_ciphersuite_t *ciphersuite, - int cert_endpoint, - uint32_t *flags) -{ - int ret = 0; - unsigned int usage = 0; - const char *ext_oid; - size_t ext_len; - - if (cert_endpoint == MBEDTLS_SSL_IS_SERVER) { - /* Server part of the key exchange */ - switch (ciphersuite->key_exchange) { - case MBEDTLS_KEY_EXCHANGE_RSA: - case MBEDTLS_KEY_EXCHANGE_RSA_PSK: - usage = MBEDTLS_X509_KU_KEY_ENCIPHERMENT; - break; - - case MBEDTLS_KEY_EXCHANGE_DHE_RSA: - case MBEDTLS_KEY_EXCHANGE_ECDHE_RSA: - case MBEDTLS_KEY_EXCHANGE_ECDHE_ECDSA: - usage = MBEDTLS_X509_KU_DIGITAL_SIGNATURE; - break; - - case MBEDTLS_KEY_EXCHANGE_ECDH_RSA: - case MBEDTLS_KEY_EXCHANGE_ECDH_ECDSA: - usage = MBEDTLS_X509_KU_KEY_AGREEMENT; - break; - - /* Don't use default: we want warnings when adding new values */ - case MBEDTLS_KEY_EXCHANGE_NONE: - case MBEDTLS_KEY_EXCHANGE_PSK: - case MBEDTLS_KEY_EXCHANGE_DHE_PSK: - case MBEDTLS_KEY_EXCHANGE_ECDHE_PSK: - case MBEDTLS_KEY_EXCHANGE_ECJPAKE: - usage = 0; - } - } else { - /* Client auth: we only implement rsa_sign and mbedtls_ecdsa_sign for now */ - usage = MBEDTLS_X509_KU_DIGITAL_SIGNATURE; - } - - if (mbedtls_x509_crt_check_key_usage(cert, usage) != 0) { - *flags |= MBEDTLS_X509_BADCERT_KEY_USAGE; - ret = -1; - } - - if (cert_endpoint == MBEDTLS_SSL_IS_SERVER) { - ext_oid = MBEDTLS_OID_SERVER_AUTH; - ext_len = MBEDTLS_OID_SIZE(MBEDTLS_OID_SERVER_AUTH); - } else { - ext_oid = MBEDTLS_OID_CLIENT_AUTH; - ext_len = MBEDTLS_OID_SIZE(MBEDTLS_OID_CLIENT_AUTH); - } - - if (mbedtls_x509_crt_check_extended_key_usage(cert, ext_oid, ext_len) != 0) { - *flags |= MBEDTLS_X509_BADCERT_EXT_KEY_USAGE; - ret = -1; - } - - return ret; -} -#endif /* MBEDTLS_X509_CRT_PARSE_C */ - #if defined(MBEDTLS_USE_PSA_CRYPTO) int mbedtls_ssl_get_handshake_transcript(mbedtls_ssl_context *ssl, const mbedtls_md_type_t md, @@ -7927,196 +7892,6 @@ static int ssl_parse_certificate_coordinate(mbedtls_ssl_context *ssl, return SSL_CERTIFICATE_EXPECTED; } -MBEDTLS_CHECK_RETURN_CRITICAL -static int ssl_parse_certificate_verify(mbedtls_ssl_context *ssl, - int authmode, - mbedtls_x509_crt *chain, - void *rs_ctx) -{ - int ret = 0; - const mbedtls_ssl_ciphersuite_t *ciphersuite_info = - ssl->handshake->ciphersuite_info; - int have_ca_chain = 0; - - int (*f_vrfy)(void *, mbedtls_x509_crt *, int, uint32_t *); - void *p_vrfy; - - if (authmode == MBEDTLS_SSL_VERIFY_NONE) { - return 0; - } - - if (ssl->f_vrfy != NULL) { - MBEDTLS_SSL_DEBUG_MSG(3, ("Use context-specific verification callback")); - f_vrfy = ssl->f_vrfy; - p_vrfy = ssl->p_vrfy; - } else { - MBEDTLS_SSL_DEBUG_MSG(3, ("Use configuration-specific verification callback")); - f_vrfy = ssl->conf->f_vrfy; - p_vrfy = ssl->conf->p_vrfy; - } - - /* - * Main check: verify certificate - */ -#if defined(MBEDTLS_X509_TRUSTED_CERTIFICATE_CALLBACK) - if (ssl->conf->f_ca_cb != NULL) { - ((void) rs_ctx); - have_ca_chain = 1; - - MBEDTLS_SSL_DEBUG_MSG(3, ("use CA callback for X.509 CRT verification")); - ret = mbedtls_x509_crt_verify_with_ca_cb( - chain, - ssl->conf->f_ca_cb, - ssl->conf->p_ca_cb, - ssl->conf->cert_profile, - ssl->hostname, - &ssl->session_negotiate->verify_result, - f_vrfy, p_vrfy); - } else -#endif /* MBEDTLS_X509_TRUSTED_CERTIFICATE_CALLBACK */ - { - mbedtls_x509_crt *ca_chain; - mbedtls_x509_crl *ca_crl; - -#if defined(MBEDTLS_SSL_SERVER_NAME_INDICATION) - if (ssl->handshake->sni_ca_chain != NULL) { - ca_chain = ssl->handshake->sni_ca_chain; - ca_crl = ssl->handshake->sni_ca_crl; - } else -#endif - { - ca_chain = ssl->conf->ca_chain; - ca_crl = ssl->conf->ca_crl; - } - - if (ca_chain != NULL) { - have_ca_chain = 1; - } - - ret = mbedtls_x509_crt_verify_restartable( - chain, - ca_chain, ca_crl, - ssl->conf->cert_profile, - ssl->hostname, - &ssl->session_negotiate->verify_result, - f_vrfy, p_vrfy, rs_ctx); - } - - if (ret != 0) { - MBEDTLS_SSL_DEBUG_RET(1, "x509_verify_cert", ret); - } - -#if defined(MBEDTLS_SSL_ECP_RESTARTABLE_ENABLED) - if (ret == MBEDTLS_ERR_ECP_IN_PROGRESS) { - return MBEDTLS_ERR_SSL_CRYPTO_IN_PROGRESS; - } -#endif - - /* - * Secondary checks: always done, but change 'ret' only if it was 0 - */ - -#if defined(MBEDTLS_PK_HAVE_ECC_KEYS) - { - const mbedtls_pk_context *pk = &chain->pk; - - /* If certificate uses an EC key, make sure the curve is OK. - * This is a public key, so it can't be opaque, so can_do() is a good - * enough check to ensure pk_ec() is safe to use here. */ - if (mbedtls_pk_can_do(pk, MBEDTLS_PK_ECKEY)) { - /* and in the unlikely case the above assumption no longer holds - * we are making sure that pk_ec() here does not return a NULL - */ - mbedtls_ecp_group_id grp_id = mbedtls_pk_get_ec_group_id(pk); - if (grp_id == MBEDTLS_ECP_DP_NONE) { - MBEDTLS_SSL_DEBUG_MSG(1, ("invalid group ID")); - return MBEDTLS_ERR_SSL_INTERNAL_ERROR; - } - if (mbedtls_ssl_check_curve(ssl, grp_id) != 0) { - ssl->session_negotiate->verify_result |= - MBEDTLS_X509_BADCERT_BAD_KEY; - - MBEDTLS_SSL_DEBUG_MSG(1, ("bad certificate (EC key curve)")); - if (ret == 0) { - ret = MBEDTLS_ERR_SSL_BAD_CERTIFICATE; - } - } - } - } -#endif /* MBEDTLS_PK_HAVE_ECC_KEYS */ - - if (mbedtls_ssl_check_cert_usage(chain, - ciphersuite_info, - !ssl->conf->endpoint, - &ssl->session_negotiate->verify_result) != 0) { - MBEDTLS_SSL_DEBUG_MSG(1, ("bad certificate (usage extensions)")); - if (ret == 0) { - ret = MBEDTLS_ERR_SSL_BAD_CERTIFICATE; - } - } - - /* mbedtls_x509_crt_verify_with_profile is supposed to report a - * verification failure through MBEDTLS_ERR_X509_CERT_VERIFY_FAILED, - * with details encoded in the verification flags. All other kinds - * of error codes, including those from the user provided f_vrfy - * functions, are treated as fatal and lead to a failure of - * ssl_parse_certificate even if verification was optional. */ - if (authmode == MBEDTLS_SSL_VERIFY_OPTIONAL && - (ret == MBEDTLS_ERR_X509_CERT_VERIFY_FAILED || - ret == MBEDTLS_ERR_SSL_BAD_CERTIFICATE)) { - ret = 0; - } - - if (have_ca_chain == 0 && authmode == MBEDTLS_SSL_VERIFY_REQUIRED) { - MBEDTLS_SSL_DEBUG_MSG(1, ("got no CA chain")); - ret = MBEDTLS_ERR_SSL_CA_CHAIN_REQUIRED; - } - - if (ret != 0) { - uint8_t alert; - - /* The certificate may have been rejected for several reasons. - Pick one and send the corresponding alert. Which alert to send - may be a subject of debate in some cases. */ - if (ssl->session_negotiate->verify_result & MBEDTLS_X509_BADCERT_OTHER) { - alert = MBEDTLS_SSL_ALERT_MSG_ACCESS_DENIED; - } else if (ssl->session_negotiate->verify_result & MBEDTLS_X509_BADCERT_CN_MISMATCH) { - alert = MBEDTLS_SSL_ALERT_MSG_BAD_CERT; - } else if (ssl->session_negotiate->verify_result & MBEDTLS_X509_BADCERT_KEY_USAGE) { - alert = MBEDTLS_SSL_ALERT_MSG_UNSUPPORTED_CERT; - } else if (ssl->session_negotiate->verify_result & MBEDTLS_X509_BADCERT_EXT_KEY_USAGE) { - alert = MBEDTLS_SSL_ALERT_MSG_UNSUPPORTED_CERT; - } else if (ssl->session_negotiate->verify_result & MBEDTLS_X509_BADCERT_NS_CERT_TYPE) { - alert = MBEDTLS_SSL_ALERT_MSG_UNSUPPORTED_CERT; - } else if (ssl->session_negotiate->verify_result & MBEDTLS_X509_BADCERT_BAD_PK) { - alert = MBEDTLS_SSL_ALERT_MSG_UNSUPPORTED_CERT; - } else if (ssl->session_negotiate->verify_result & MBEDTLS_X509_BADCERT_BAD_KEY) { - alert = MBEDTLS_SSL_ALERT_MSG_UNSUPPORTED_CERT; - } else if (ssl->session_negotiate->verify_result & MBEDTLS_X509_BADCERT_EXPIRED) { - alert = MBEDTLS_SSL_ALERT_MSG_CERT_EXPIRED; - } else if (ssl->session_negotiate->verify_result & MBEDTLS_X509_BADCERT_REVOKED) { - alert = MBEDTLS_SSL_ALERT_MSG_CERT_REVOKED; - } else if (ssl->session_negotiate->verify_result & MBEDTLS_X509_BADCERT_NOT_TRUSTED) { - alert = MBEDTLS_SSL_ALERT_MSG_UNKNOWN_CA; - } else { - alert = MBEDTLS_SSL_ALERT_MSG_CERT_UNKNOWN; - } - mbedtls_ssl_send_alert_message(ssl, MBEDTLS_SSL_ALERT_LEVEL_FATAL, - alert); - } - -#if defined(MBEDTLS_DEBUG_C) - if (ssl->session_negotiate->verify_result != 0) { - MBEDTLS_SSL_DEBUG_MSG(3, ("! Certificate verification flags %08x", - (unsigned int) ssl->session_negotiate->verify_result)); - } else { - MBEDTLS_SSL_DEBUG_MSG(3, ("Certificate verification flags clear")); - } -#endif /* MBEDTLS_DEBUG_C */ - - return ret; -} - #if !defined(MBEDTLS_SSL_KEEP_PEER_CERTIFICATE) MBEDTLS_CHECK_RETURN_CRITICAL static int ssl_remember_peer_crt_digest(mbedtls_ssl_context *ssl, @@ -8173,6 +7948,7 @@ int mbedtls_ssl_parse_certificate(mbedtls_ssl_context *ssl) { int ret = 0; int crt_expected; + /* Authmode: precedence order is SNI if used else configuration */ #if defined(MBEDTLS_SSL_SRV_C) && defined(MBEDTLS_SSL_SERVER_NAME_INDICATION) const int authmode = ssl->handshake->sni_authmode != MBEDTLS_SSL_VERIFY_UNSET ? ssl->handshake->sni_authmode @@ -8252,8 +8028,9 @@ crt_verify: } #endif - ret = ssl_parse_certificate_verify(ssl, authmode, - chain, rs_ctx); + ret = mbedtls_ssl_verify_certificate(ssl, authmode, chain, + ssl->handshake->ciphersuite_info, + rs_ctx); if (ret != 0) { goto exit; } @@ -9919,4 +9696,274 @@ int mbedtls_ssl_session_set_ticket_alpn(mbedtls_ssl_session *session, return 0; } #endif /* MBEDTLS_SSL_SRV_C && MBEDTLS_SSL_EARLY_DATA && MBEDTLS_SSL_ALPN */ + +/* + * The following functions are used by 1.2 and 1.3, client and server. + */ +#if defined(MBEDTLS_SSL_HANDSHAKE_WITH_CERT_ENABLED) +int mbedtls_ssl_check_cert_usage(const mbedtls_x509_crt *cert, + const mbedtls_ssl_ciphersuite_t *ciphersuite, + int recv_endpoint, + mbedtls_ssl_protocol_version tls_version, + uint32_t *flags) +{ + int ret = 0; + unsigned int usage = 0; + const char *ext_oid; + size_t ext_len; + + /* + * keyUsage + */ + + /* Note: don't guard this with MBEDTLS_SSL_CLI_C because the server wants + * to check what a compliant client will think while choosing which cert + * to send to the client. */ +#if defined(MBEDTLS_SSL_PROTO_TLS1_2) + if (tls_version == MBEDTLS_SSL_VERSION_TLS1_2 && + recv_endpoint == MBEDTLS_SSL_IS_CLIENT) { + /* TLS 1.2 server part of the key exchange */ + switch (ciphersuite->key_exchange) { + case MBEDTLS_KEY_EXCHANGE_RSA: + case MBEDTLS_KEY_EXCHANGE_RSA_PSK: + usage = MBEDTLS_X509_KU_KEY_ENCIPHERMENT; + break; + + case MBEDTLS_KEY_EXCHANGE_DHE_RSA: + case MBEDTLS_KEY_EXCHANGE_ECDHE_RSA: + case MBEDTLS_KEY_EXCHANGE_ECDHE_ECDSA: + usage = MBEDTLS_X509_KU_DIGITAL_SIGNATURE; + break; + + case MBEDTLS_KEY_EXCHANGE_ECDH_RSA: + case MBEDTLS_KEY_EXCHANGE_ECDH_ECDSA: + usage = MBEDTLS_X509_KU_KEY_AGREEMENT; + break; + + /* Don't use default: we want warnings when adding new values */ + case MBEDTLS_KEY_EXCHANGE_NONE: + case MBEDTLS_KEY_EXCHANGE_PSK: + case MBEDTLS_KEY_EXCHANGE_DHE_PSK: + case MBEDTLS_KEY_EXCHANGE_ECDHE_PSK: + case MBEDTLS_KEY_EXCHANGE_ECJPAKE: + usage = 0; + } + } else +#endif + { + /* This is either TLS 1.3 authentication, which always uses signatures, + * or 1.2 client auth: rsa_sign and mbedtls_ecdsa_sign are the only + * options we implement, both using signatures. */ + (void) tls_version; + (void) ciphersuite; + usage = MBEDTLS_X509_KU_DIGITAL_SIGNATURE; + } + + if (mbedtls_x509_crt_check_key_usage(cert, usage) != 0) { + *flags |= MBEDTLS_X509_BADCERT_KEY_USAGE; + ret = -1; + } + + /* + * extKeyUsage + */ + + if (recv_endpoint == MBEDTLS_SSL_IS_CLIENT) { + ext_oid = MBEDTLS_OID_SERVER_AUTH; + ext_len = MBEDTLS_OID_SIZE(MBEDTLS_OID_SERVER_AUTH); + } else { + ext_oid = MBEDTLS_OID_CLIENT_AUTH; + ext_len = MBEDTLS_OID_SIZE(MBEDTLS_OID_CLIENT_AUTH); + } + + if (mbedtls_x509_crt_check_extended_key_usage(cert, ext_oid, ext_len) != 0) { + *flags |= MBEDTLS_X509_BADCERT_EXT_KEY_USAGE; + ret = -1; + } + + return ret; +} + +int mbedtls_ssl_verify_certificate(mbedtls_ssl_context *ssl, + int authmode, + mbedtls_x509_crt *chain, + const mbedtls_ssl_ciphersuite_t *ciphersuite_info, + void *rs_ctx) +{ + if (authmode == MBEDTLS_SSL_VERIFY_NONE) { + return 0; + } + + /* + * Primary check: use the appropriate X.509 verification function + */ + int (*f_vrfy)(void *, mbedtls_x509_crt *, int, uint32_t *); + void *p_vrfy; + if (ssl->f_vrfy != NULL) { + MBEDTLS_SSL_DEBUG_MSG(3, ("Use context-specific verification callback")); + f_vrfy = ssl->f_vrfy; + p_vrfy = ssl->p_vrfy; + } else { + MBEDTLS_SSL_DEBUG_MSG(3, ("Use configuration-specific verification callback")); + f_vrfy = ssl->conf->f_vrfy; + p_vrfy = ssl->conf->p_vrfy; + } + + int ret = 0; + int have_ca_chain_or_callback = 0; +#if defined(MBEDTLS_X509_TRUSTED_CERTIFICATE_CALLBACK) + if (ssl->conf->f_ca_cb != NULL) { + ((void) rs_ctx); + have_ca_chain_or_callback = 1; + + MBEDTLS_SSL_DEBUG_MSG(3, ("use CA callback for X.509 CRT verification")); + ret = mbedtls_x509_crt_verify_with_ca_cb( + chain, + ssl->conf->f_ca_cb, + ssl->conf->p_ca_cb, + ssl->conf->cert_profile, + ssl->hostname, + &ssl->session_negotiate->verify_result, + f_vrfy, p_vrfy); + } else +#endif /* MBEDTLS_X509_TRUSTED_CERTIFICATE_CALLBACK */ + { + mbedtls_x509_crt *ca_chain; + mbedtls_x509_crl *ca_crl; +#if defined(MBEDTLS_SSL_SERVER_NAME_INDICATION) + if (ssl->handshake->sni_ca_chain != NULL) { + ca_chain = ssl->handshake->sni_ca_chain; + ca_crl = ssl->handshake->sni_ca_crl; + } else +#endif + { + ca_chain = ssl->conf->ca_chain; + ca_crl = ssl->conf->ca_crl; + } + + if (ca_chain != NULL) { + have_ca_chain_or_callback = 1; + } + + ret = mbedtls_x509_crt_verify_restartable( + chain, + ca_chain, ca_crl, + ssl->conf->cert_profile, + ssl->hostname, + &ssl->session_negotiate->verify_result, + f_vrfy, p_vrfy, rs_ctx); + } + + if (ret != 0) { + MBEDTLS_SSL_DEBUG_RET(1, "x509_verify_cert", ret); + } + +#if defined(MBEDTLS_SSL_ECP_RESTARTABLE_ENABLED) + if (ret == MBEDTLS_ERR_ECP_IN_PROGRESS) { + return MBEDTLS_ERR_SSL_CRYPTO_IN_PROGRESS; + } +#endif + + /* + * Secondary checks: always done, but change 'ret' only if it was 0 + */ + + /* With TLS 1.2 and ECC certs, check that the curve used by the + * certificate is on our list of acceptable curves. + * + * With TLS 1.3 this is not needed because the curve is part of the + * signature algorithm (eg ecdsa_secp256r1_sha256) which is checked when + * we validate the signature made with the key associated to this cert. + */ +#if defined(MBEDTLS_SSL_PROTO_TLS1_2) && \ + defined(MBEDTLS_PK_HAVE_ECC_KEYS) + if (ssl->tls_version == MBEDTLS_SSL_VERSION_TLS1_2 && + mbedtls_pk_can_do(&chain->pk, MBEDTLS_PK_ECKEY)) { + if (mbedtls_ssl_check_curve(ssl, mbedtls_pk_get_ec_group_id(&chain->pk)) != 0) { + MBEDTLS_SSL_DEBUG_MSG(1, ("bad certificate (EC key curve)")); + ssl->session_negotiate->verify_result |= MBEDTLS_X509_BADCERT_BAD_KEY; + if (ret == 0) { + ret = MBEDTLS_ERR_SSL_BAD_CERTIFICATE; + } + } + } +#endif /* MBEDTLS_SSL_PROTO_TLS1_2 && MBEDTLS_PK_HAVE_ECC_KEYS */ + + /* Check X.509 usage extensions (keyUsage, extKeyUsage) */ + if (mbedtls_ssl_check_cert_usage(chain, + ciphersuite_info, + ssl->conf->endpoint, + ssl->tls_version, + &ssl->session_negotiate->verify_result) != 0) { + MBEDTLS_SSL_DEBUG_MSG(1, ("bad certificate (usage extensions)")); + if (ret == 0) { + ret = MBEDTLS_ERR_SSL_BAD_CERTIFICATE; + } + } + + /* With authmode optional, we want to keep going if the certificate was + * unacceptable, but still fail on other errors (out of memory etc), + * including fatal errors from the f_vrfy callback. + * + * The only acceptable errors are: + * - MBEDTLS_ERR_X509_CERT_VERIFY_FAILED: cert rejected by primary check; + * - MBEDTLS_ERR_SSL_BAD_CERTIFICATE: cert rejected by secondary checks. + * Anything else is a fatal error. */ + if (authmode == MBEDTLS_SSL_VERIFY_OPTIONAL && + (ret == MBEDTLS_ERR_X509_CERT_VERIFY_FAILED || + ret == MBEDTLS_ERR_SSL_BAD_CERTIFICATE)) { + ret = 0; + } + + /* Return a specific error as this is a user error: inconsistent + * configuration - can't verify without trust anchors. */ + if (have_ca_chain_or_callback == 0 && authmode == MBEDTLS_SSL_VERIFY_REQUIRED) { + MBEDTLS_SSL_DEBUG_MSG(1, ("got no CA chain")); + ret = MBEDTLS_ERR_SSL_CA_CHAIN_REQUIRED; + } + + if (ret != 0) { + uint8_t alert; + + /* The certificate may have been rejected for several reasons. + Pick one and send the corresponding alert. Which alert to send + may be a subject of debate in some cases. */ + if (ssl->session_negotiate->verify_result & MBEDTLS_X509_BADCERT_OTHER) { + alert = MBEDTLS_SSL_ALERT_MSG_ACCESS_DENIED; + } else if (ssl->session_negotiate->verify_result & MBEDTLS_X509_BADCERT_CN_MISMATCH) { + alert = MBEDTLS_SSL_ALERT_MSG_BAD_CERT; + } else if (ssl->session_negotiate->verify_result & MBEDTLS_X509_BADCERT_KEY_USAGE) { + alert = MBEDTLS_SSL_ALERT_MSG_UNSUPPORTED_CERT; + } else if (ssl->session_negotiate->verify_result & MBEDTLS_X509_BADCERT_EXT_KEY_USAGE) { + alert = MBEDTLS_SSL_ALERT_MSG_UNSUPPORTED_CERT; + } else if (ssl->session_negotiate->verify_result & MBEDTLS_X509_BADCERT_BAD_PK) { + alert = MBEDTLS_SSL_ALERT_MSG_UNSUPPORTED_CERT; + } else if (ssl->session_negotiate->verify_result & MBEDTLS_X509_BADCERT_BAD_KEY) { + alert = MBEDTLS_SSL_ALERT_MSG_UNSUPPORTED_CERT; + } else if (ssl->session_negotiate->verify_result & MBEDTLS_X509_BADCERT_EXPIRED) { + alert = MBEDTLS_SSL_ALERT_MSG_CERT_EXPIRED; + } else if (ssl->session_negotiate->verify_result & MBEDTLS_X509_BADCERT_REVOKED) { + alert = MBEDTLS_SSL_ALERT_MSG_CERT_REVOKED; + } else if (ssl->session_negotiate->verify_result & MBEDTLS_X509_BADCERT_NOT_TRUSTED) { + alert = MBEDTLS_SSL_ALERT_MSG_UNKNOWN_CA; + } else { + alert = MBEDTLS_SSL_ALERT_MSG_CERT_UNKNOWN; + } + mbedtls_ssl_send_alert_message(ssl, MBEDTLS_SSL_ALERT_LEVEL_FATAL, + alert); + } + +#if defined(MBEDTLS_DEBUG_C) + if (ssl->session_negotiate->verify_result != 0) { + MBEDTLS_SSL_DEBUG_MSG(3, ("! Certificate verification flags %08x", + (unsigned int) ssl->session_negotiate->verify_result)); + } else { + MBEDTLS_SSL_DEBUG_MSG(3, ("Certificate verification flags clear")); + } +#endif /* MBEDTLS_DEBUG_C */ + + return ret; +} +#endif /* MBEDTLS_SSL_HANDSHAKE_WITH_CERT_ENABLED */ + #endif /* MBEDTLS_SSL_TLS_C */ diff --git a/thirdparty/mbedtls/library/ssl_tls12_client.c b/thirdparty/mbedtls/library/ssl_tls12_client.c index eac6a3aadd..9b2da5a39d 100644 --- a/thirdparty/mbedtls/library/ssl_tls12_client.c +++ b/thirdparty/mbedtls/library/ssl_tls12_client.c @@ -364,7 +364,8 @@ static int ssl_write_session_ticket_ext(mbedtls_ssl_context *ssl, *olen = 0; - if (ssl->conf->session_tickets == MBEDTLS_SSL_SESSION_TICKETS_DISABLED) { + if (mbedtls_ssl_conf_get_session_tickets(ssl->conf) == + MBEDTLS_SSL_SESSION_TICKETS_DISABLED) { return 0; } @@ -787,7 +788,8 @@ static int ssl_parse_session_ticket_ext(mbedtls_ssl_context *ssl, const unsigned char *buf, size_t len) { - if (ssl->conf->session_tickets == MBEDTLS_SSL_SESSION_TICKETS_DISABLED || + if ((mbedtls_ssl_conf_get_session_tickets(ssl->conf) == + MBEDTLS_SSL_SESSION_TICKETS_DISABLED) || len != 0) { MBEDTLS_SSL_DEBUG_MSG(1, ("non-matching session ticket extension")); diff --git a/thirdparty/mbedtls/library/ssl_tls12_server.c b/thirdparty/mbedtls/library/ssl_tls12_server.c index b49a8ae6a6..03722ac33c 100644 --- a/thirdparty/mbedtls/library/ssl_tls12_server.c +++ b/thirdparty/mbedtls/library/ssl_tls12_server.c @@ -756,7 +756,9 @@ static int ssl_pick_cert(mbedtls_ssl_context *ssl, * and decrypting with the same RSA key. */ if (mbedtls_ssl_check_cert_usage(cur->cert, ciphersuite_info, - MBEDTLS_SSL_IS_SERVER, &flags) != 0) { + MBEDTLS_SSL_IS_CLIENT, + MBEDTLS_SSL_VERSION_TLS1_2, + &flags) != 0) { MBEDTLS_SSL_DEBUG_MSG(3, ("certificate mismatch: " "(extended) key usage extension")); continue; @@ -2631,13 +2633,8 @@ static int ssl_get_ecdh_params_from_cert(mbedtls_ssl_context *ssl) ssl->handshake->xxdh_psa_type = psa_get_key_type(&key_attributes); ssl->handshake->xxdh_psa_bits = psa_get_key_bits(&key_attributes); - if (pk_type == MBEDTLS_PK_OPAQUE) { - /* Opaque key is created by the user (externally from Mbed TLS) - * so we assume it already has the right algorithm and flags - * set. Just copy its ID as reference. */ - ssl->handshake->xxdh_psa_privkey = pk->priv_id; - ssl->handshake->xxdh_psa_privkey_is_external = 1; - } else { +#if defined(MBEDTLS_PK_USE_PSA_EC_DATA) + if (pk_type != MBEDTLS_PK_OPAQUE) { /* PK_ECKEY[_DH] and PK_ECDSA instead as parsed from the PK * module and only have ECDSA capabilities. Since we need * them for ECDH later, we export and then re-import them with @@ -2665,10 +2662,20 @@ static int ssl_get_ecdh_params_from_cert(mbedtls_ssl_context *ssl) /* Set this key as owned by the TLS library: it will be its duty * to clear it exit. */ ssl->handshake->xxdh_psa_privkey_is_external = 0; + + ret = 0; + break; } +#endif /* MBEDTLS_PK_USE_PSA_EC_DATA */ + /* Opaque key is created by the user (externally from Mbed TLS) + * so we assume it already has the right algorithm and flags + * set. Just copy its ID as reference. */ + ssl->handshake->xxdh_psa_privkey = pk->priv_id; + ssl->handshake->xxdh_psa_privkey_is_external = 1; ret = 0; break; + #if !defined(MBEDTLS_PK_USE_PSA_EC_DATA) case MBEDTLS_PK_ECKEY: case MBEDTLS_PK_ECKEY_DH: @@ -3916,7 +3923,7 @@ static int ssl_parse_client_key_exchange(mbedtls_ssl_context *ssl) #if defined(MBEDTLS_USE_PSA_CRYPTO) psa_status_t status = PSA_ERROR_CORRUPTION_DETECTED; psa_status_t destruction_status = PSA_ERROR_CORRUPTION_DETECTED; - uint8_t ecpoint_len; + size_t ecpoint_len; mbedtls_ssl_handshake_params *handshake = ssl->handshake; diff --git a/thirdparty/mbedtls/library/ssl_tls13_client.c b/thirdparty/mbedtls/library/ssl_tls13_client.c index 7fcc394319..b63b5e63c5 100644 --- a/thirdparty/mbedtls/library/ssl_tls13_client.c +++ b/thirdparty/mbedtls/library/ssl_tls13_client.c @@ -666,6 +666,7 @@ static int ssl_tls13_write_psk_key_exchange_modes_ext(mbedtls_ssl_context *ssl, return 0; } +#if defined(MBEDTLS_SSL_SESSION_TICKETS) static psa_algorithm_t ssl_tls13_get_ciphersuite_hash_alg(int ciphersuite) { const mbedtls_ssl_ciphersuite_t *ciphersuite_info = NULL; @@ -678,7 +679,6 @@ static psa_algorithm_t ssl_tls13_get_ciphersuite_hash_alg(int ciphersuite) return PSA_ALG_NONE; } -#if defined(MBEDTLS_SSL_SESSION_TICKETS) static int ssl_tls13_has_configured_ticket(mbedtls_ssl_context *ssl) { mbedtls_ssl_session *session = ssl->session_negotiate; @@ -1141,6 +1141,11 @@ int mbedtls_ssl_tls13_write_client_hello_exts(mbedtls_ssl_context *ssl, *out_len = 0; + ret = mbedtls_ssl_tls13_crypto_init(ssl); + if (ret != 0) { + return ret; + } + /* Write supported_versions extension * * Supported Versions Extension is mandatory with TLS 1.3. diff --git a/thirdparty/mbedtls/library/ssl_tls13_generic.c b/thirdparty/mbedtls/library/ssl_tls13_generic.c index d448a054a9..b6d09788ba 100644 --- a/thirdparty/mbedtls/library/ssl_tls13_generic.c +++ b/thirdparty/mbedtls/library/ssl_tls13_generic.c @@ -27,7 +27,6 @@ #include "psa/crypto.h" #include "psa_util_internal.h" -#if defined(MBEDTLS_SSL_TLS1_3_KEY_EXCHANGE_MODE_SOME_EPHEMERAL_ENABLED) /* Define a local translating function to save code size by not using too many * arguments in each translating place. */ static int local_err_translation(psa_status_t status) @@ -37,7 +36,16 @@ static int local_err_translation(psa_status_t status) psa_generic_status_to_mbedtls); } #define PSA_TO_MBEDTLS_ERR(status) local_err_translation(status) -#endif + +int mbedtls_ssl_tls13_crypto_init(mbedtls_ssl_context *ssl) +{ + psa_status_t status = psa_crypto_init(); + if (status != PSA_SUCCESS) { + (void) ssl; // unused when debugging is disabled + MBEDTLS_SSL_DEBUG_RET(1, "psa_crypto_init", status); + } + return PSA_TO_MBEDTLS_ERR(status); +} const uint8_t mbedtls_ssl_tls13_hello_retry_request_magic[ MBEDTLS_SERVER_HELLO_RANDOM_LEN] = @@ -193,10 +201,12 @@ static void ssl_tls13_create_verify_structure(const unsigned char *transcript_ha idx = 64; if (from == MBEDTLS_SSL_IS_CLIENT) { - memcpy(verify_buffer + idx, MBEDTLS_SSL_TLS1_3_LBL_WITH_LEN(client_cv)); + memcpy(verify_buffer + idx, mbedtls_ssl_tls13_labels.client_cv, + MBEDTLS_SSL_TLS1_3_LBL_LEN(client_cv)); idx += MBEDTLS_SSL_TLS1_3_LBL_LEN(client_cv); } else { /* from == MBEDTLS_SSL_IS_SERVER */ - memcpy(verify_buffer + idx, MBEDTLS_SSL_TLS1_3_LBL_WITH_LEN(server_cv)); + memcpy(verify_buffer + idx, mbedtls_ssl_tls13_labels.server_cv, + MBEDTLS_SSL_TLS1_3_LBL_LEN(server_cv)); idx += MBEDTLS_SSL_TLS1_3_LBL_LEN(server_cv); } @@ -470,6 +480,7 @@ int mbedtls_ssl_tls13_parse_certificate(mbedtls_ssl_context *ssl, mbedtls_free(ssl->session_negotiate->peer_cert); } + /* This is used by ssl_tls13_validate_certificate() */ if (certificate_list_len == 0) { ssl->session_negotiate->peer_cert = NULL; ret = 0; @@ -625,25 +636,13 @@ int mbedtls_ssl_tls13_parse_certificate(mbedtls_ssl_context *ssl, MBEDTLS_CHECK_RETURN_CRITICAL static int ssl_tls13_validate_certificate(mbedtls_ssl_context *ssl) { - int ret = 0; - int authmode = MBEDTLS_SSL_VERIFY_REQUIRED; - mbedtls_x509_crt *ca_chain; - mbedtls_x509_crl *ca_crl; - const char *ext_oid; - size_t ext_len; - uint32_t verify_result = 0; - - /* If SNI was used, overwrite authentication mode - * from the configuration. */ -#if defined(MBEDTLS_SSL_SRV_C) - if (ssl->conf->endpoint == MBEDTLS_SSL_IS_SERVER) { -#if defined(MBEDTLS_SSL_SERVER_NAME_INDICATION) - if (ssl->handshake->sni_authmode != MBEDTLS_SSL_VERIFY_UNSET) { - authmode = ssl->handshake->sni_authmode; - } else -#endif - authmode = ssl->conf->authmode; - } + /* Authmode: precedence order is SNI if used else configuration */ +#if defined(MBEDTLS_SSL_SRV_C) && defined(MBEDTLS_SSL_SERVER_NAME_INDICATION) + const int authmode = ssl->handshake->sni_authmode != MBEDTLS_SSL_VERIFY_UNSET + ? ssl->handshake->sni_authmode + : ssl->conf->authmode; +#else + const int authmode = ssl->conf->authmode; #endif /* @@ -675,6 +674,11 @@ static int ssl_tls13_validate_certificate(mbedtls_ssl_context *ssl) #endif /* MBEDTLS_SSL_SRV_C */ #if defined(MBEDTLS_SSL_CLI_C) + /* Regardless of authmode, the server is not allowed to send an empty + * certificate chain. (Last paragraph before 4.4.2.1 in RFC 8446: "The + * server's certificate_list MUST always be non-empty.") With authmode + * optional/none, we continue the handshake if we can't validate the + * server's cert, but we still break it if no certificate was sent. */ if (ssl->conf->endpoint == MBEDTLS_SSL_IS_CLIENT) { MBEDTLS_SSL_PEND_FATAL_ALERT(MBEDTLS_SSL_ALERT_MSG_NO_CERT, MBEDTLS_ERR_SSL_FATAL_ALERT_MESSAGE); @@ -683,114 +687,9 @@ static int ssl_tls13_validate_certificate(mbedtls_ssl_context *ssl) #endif /* MBEDTLS_SSL_CLI_C */ } -#if defined(MBEDTLS_SSL_SERVER_NAME_INDICATION) - if (ssl->handshake->sni_ca_chain != NULL) { - ca_chain = ssl->handshake->sni_ca_chain; - ca_crl = ssl->handshake->sni_ca_crl; - } else -#endif /* MBEDTLS_SSL_SERVER_NAME_INDICATION */ - { - ca_chain = ssl->conf->ca_chain; - ca_crl = ssl->conf->ca_crl; - } - - /* - * Main check: verify certificate - */ - ret = mbedtls_x509_crt_verify_with_profile( - ssl->session_negotiate->peer_cert, - ca_chain, ca_crl, - ssl->conf->cert_profile, - ssl->hostname, - &verify_result, - ssl->conf->f_vrfy, ssl->conf->p_vrfy); - - if (ret != 0) { - MBEDTLS_SSL_DEBUG_RET(1, "x509_verify_cert", ret); - } - - /* - * Secondary checks: always done, but change 'ret' only if it was 0 - */ - if (ssl->conf->endpoint == MBEDTLS_SSL_IS_CLIENT) { - ext_oid = MBEDTLS_OID_SERVER_AUTH; - ext_len = MBEDTLS_OID_SIZE(MBEDTLS_OID_SERVER_AUTH); - } else { - ext_oid = MBEDTLS_OID_CLIENT_AUTH; - ext_len = MBEDTLS_OID_SIZE(MBEDTLS_OID_CLIENT_AUTH); - } - - if ((mbedtls_x509_crt_check_key_usage( - ssl->session_negotiate->peer_cert, - MBEDTLS_X509_KU_DIGITAL_SIGNATURE) != 0) || - (mbedtls_x509_crt_check_extended_key_usage( - ssl->session_negotiate->peer_cert, - ext_oid, ext_len) != 0)) { - MBEDTLS_SSL_DEBUG_MSG(1, ("bad certificate (usage extensions)")); - if (ret == 0) { - ret = MBEDTLS_ERR_SSL_BAD_CERTIFICATE; - } - } - - /* mbedtls_x509_crt_verify_with_profile is supposed to report a - * verification failure through MBEDTLS_ERR_X509_CERT_VERIFY_FAILED, - * with details encoded in the verification flags. All other kinds - * of error codes, including those from the user provided f_vrfy - * functions, are treated as fatal and lead to a failure of - * mbedtls_ssl_tls13_parse_certificate even if verification was optional. - */ - if (authmode == MBEDTLS_SSL_VERIFY_OPTIONAL && - (ret == MBEDTLS_ERR_X509_CERT_VERIFY_FAILED || - ret == MBEDTLS_ERR_SSL_BAD_CERTIFICATE)) { - ret = 0; - } - - if (ca_chain == NULL && authmode == MBEDTLS_SSL_VERIFY_REQUIRED) { - MBEDTLS_SSL_DEBUG_MSG(1, ("got no CA chain")); - ret = MBEDTLS_ERR_SSL_CA_CHAIN_REQUIRED; - } - - if (ret != 0) { - /* The certificate may have been rejected for several reasons. - Pick one and send the corresponding alert. Which alert to send - may be a subject of debate in some cases. */ - if (verify_result & MBEDTLS_X509_BADCERT_OTHER) { - MBEDTLS_SSL_PEND_FATAL_ALERT( - MBEDTLS_SSL_ALERT_MSG_ACCESS_DENIED, ret); - } else if (verify_result & MBEDTLS_X509_BADCERT_CN_MISMATCH) { - MBEDTLS_SSL_PEND_FATAL_ALERT(MBEDTLS_SSL_ALERT_MSG_BAD_CERT, ret); - } else if (verify_result & (MBEDTLS_X509_BADCERT_KEY_USAGE | - MBEDTLS_X509_BADCERT_EXT_KEY_USAGE | - MBEDTLS_X509_BADCERT_NS_CERT_TYPE | - MBEDTLS_X509_BADCERT_BAD_PK | - MBEDTLS_X509_BADCERT_BAD_KEY)) { - MBEDTLS_SSL_PEND_FATAL_ALERT( - MBEDTLS_SSL_ALERT_MSG_UNSUPPORTED_CERT, ret); - } else if (verify_result & MBEDTLS_X509_BADCERT_EXPIRED) { - MBEDTLS_SSL_PEND_FATAL_ALERT( - MBEDTLS_SSL_ALERT_MSG_CERT_EXPIRED, ret); - } else if (verify_result & MBEDTLS_X509_BADCERT_REVOKED) { - MBEDTLS_SSL_PEND_FATAL_ALERT( - MBEDTLS_SSL_ALERT_MSG_CERT_REVOKED, ret); - } else if (verify_result & MBEDTLS_X509_BADCERT_NOT_TRUSTED) { - MBEDTLS_SSL_PEND_FATAL_ALERT(MBEDTLS_SSL_ALERT_MSG_UNKNOWN_CA, ret); - } else { - MBEDTLS_SSL_PEND_FATAL_ALERT( - MBEDTLS_SSL_ALERT_MSG_CERT_UNKNOWN, ret); - } - } - -#if defined(MBEDTLS_DEBUG_C) - if (verify_result != 0) { - MBEDTLS_SSL_DEBUG_MSG(3, ("! Certificate verification flags %08x", - (unsigned int) verify_result)); - } else { - MBEDTLS_SSL_DEBUG_MSG(3, ("Certificate verification flags clear")); - } -#endif /* MBEDTLS_DEBUG_C */ - - ssl->session_negotiate->verify_result = verify_result; - return ret; + return mbedtls_ssl_verify_certificate(ssl, authmode, + ssl->session_negotiate->peer_cert, + NULL, NULL); } #else /* MBEDTLS_SSL_KEEP_PEER_CERTIFICATE */ MBEDTLS_CHECK_RETURN_CRITICAL @@ -1482,9 +1381,11 @@ int mbedtls_ssl_tls13_check_early_data_len(mbedtls_ssl_context *ssl, ssl->total_early_data_size)) { MBEDTLS_SSL_DEBUG_MSG( - 2, ("EarlyData: Too much early data received, %u + %" MBEDTLS_PRINTF_SIZET " > %u", - ssl->total_early_data_size, early_data_len, - ssl->session_negotiate->max_early_data_size)); + 2, ("EarlyData: Too much early data received, " + "%lu + %" MBEDTLS_PRINTF_SIZET " > %lu", + (unsigned long) ssl->total_early_data_size, + early_data_len, + (unsigned long) ssl->session_negotiate->max_early_data_size)); MBEDTLS_SSL_PEND_FATAL_ALERT( MBEDTLS_SSL_ALERT_MSG_UNEXPECTED_MESSAGE, diff --git a/thirdparty/mbedtls/library/ssl_tls13_server.c b/thirdparty/mbedtls/library/ssl_tls13_server.c index 2760d76a5d..693edc7b0b 100644 --- a/thirdparty/mbedtls/library/ssl_tls13_server.c +++ b/thirdparty/mbedtls/library/ssl_tls13_server.c @@ -92,8 +92,9 @@ static void ssl_tls13_select_ciphersuite( return; } - MBEDTLS_SSL_DEBUG_MSG(2, ("No matched ciphersuite, psk_ciphersuite_id=%x, psk_hash_alg=%x", - (unsigned) psk_ciphersuite_id, psk_hash_alg)); + MBEDTLS_SSL_DEBUG_MSG(2, ("No matched ciphersuite, psk_ciphersuite_id=%x, psk_hash_alg=%lx", + (unsigned) psk_ciphersuite_id, + (unsigned long) psk_hash_alg)); } #if defined(MBEDTLS_SSL_TLS1_3_KEY_EXCHANGE_MODE_SOME_PSK_ENABLED) @@ -172,12 +173,12 @@ static int ssl_tls13_parse_key_exchange_modes_ext(mbedtls_ssl_context *ssl, #define SSL_TLS1_3_PSK_IDENTITY_MATCH_BUT_PSK_NOT_USABLE 1 #define SSL_TLS1_3_PSK_IDENTITY_MATCH 0 -#if defined(MBEDTLS_SSL_SESSION_TICKETS) MBEDTLS_CHECK_RETURN_CRITICAL static int ssl_tls13_key_exchange_is_psk_available(mbedtls_ssl_context *ssl); MBEDTLS_CHECK_RETURN_CRITICAL static int ssl_tls13_key_exchange_is_psk_ephemeral_available(mbedtls_ssl_context *ssl); +#if defined(MBEDTLS_SSL_SESSION_TICKETS) MBEDTLS_CHECK_RETURN_CRITICAL static int ssl_tls13_offered_psks_check_identity_match_ticket( mbedtls_ssl_context *ssl, @@ -575,10 +576,8 @@ static int ssl_tls13_parse_pre_shared_key_ext( psa_algorithm_t psk_hash_alg; int allowed_key_exchange_modes; -#if defined(MBEDTLS_SSL_SESSION_TICKETS) mbedtls_ssl_session session; mbedtls_ssl_session_init(&session); -#endif MBEDTLS_SSL_CHK_BUF_READ_PTR(p_identity_len, identities_end, 2 + 1 + 4); identity_len = MBEDTLS_GET_UINT16_BE(p_identity_len, 0); @@ -1356,19 +1355,23 @@ static int ssl_tls13_parse_client_hello(mbedtls_ssl_context *ssl, * compression methods and the length of the extensions. * * cipher_suites cipher_suites_len bytes - * legacy_compression_methods 2 bytes - * extensions_len 2 bytes + * legacy_compression_methods length 1 byte */ - MBEDTLS_SSL_CHK_BUF_READ_PTR(p, end, cipher_suites_len + 2 + 2); + MBEDTLS_SSL_CHK_BUF_READ_PTR(p, end, cipher_suites_len + 1); p += cipher_suites_len; cipher_suites_end = p; + /* Check if we have enough data for legacy_compression_methods + * and the length of the extensions (2 bytes). + */ + MBEDTLS_SSL_CHK_BUF_READ_PTR(p + 1, end, p[0] + 2); + /* * Search for the supported versions extension and parse it to determine * if the client supports TLS 1.3. */ ret = mbedtls_ssl_tls13_is_supported_versions_ext_present_in_exts( - ssl, p + 2, end, + ssl, p + 1 + p[0], end, &supported_versions_data, &supported_versions_data_end); if (ret < 0) { MBEDTLS_SSL_DEBUG_RET(1, @@ -1409,6 +1412,12 @@ static int ssl_tls13_parse_client_hello(mbedtls_ssl_context *ssl, ssl->session_negotiate->tls_version = MBEDTLS_SSL_VERSION_TLS1_3; ssl->session_negotiate->endpoint = ssl->conf->endpoint; + /* Before doing any crypto, make sure we can. */ + ret = mbedtls_ssl_tls13_crypto_init(ssl); + if (ret != 0) { + return ret; + } + /* * We are negotiating the version 1.3 of the protocol. Do what we have * postponed: copy of the client random bytes, copy of the legacy session @@ -3109,6 +3118,7 @@ static int ssl_tls13_handshake_wrapup(mbedtls_ssl_context *ssl) return 0; } +#if defined(MBEDTLS_SSL_SESSION_TICKETS) /* * Handler for MBEDTLS_SSL_TLS1_3_NEW_SESSION_TICKET */ @@ -3138,7 +3148,6 @@ static int ssl_tls13_write_new_session_ticket_coordinate(mbedtls_ssl_context *ss return SSL_NEW_SESSION_TICKET_WRITE; } -#if defined(MBEDTLS_SSL_SESSION_TICKETS) MBEDTLS_CHECK_RETURN_CRITICAL static int ssl_tls13_prepare_new_session_ticket(mbedtls_ssl_context *ssl, unsigned char *ticket_nonce, diff --git a/thirdparty/mbedtls/library/version_features.c b/thirdparty/mbedtls/library/version_features.c index 406161d4c7..f542d9808f 100644 --- a/thirdparty/mbedtls/library/version_features.c +++ b/thirdparty/mbedtls/library/version_features.c @@ -423,6 +423,9 @@ static const char * const features[] = { #if defined(MBEDTLS_PSA_CRYPTO_SPM) "PSA_CRYPTO_SPM", //no-check-names #endif /* MBEDTLS_PSA_CRYPTO_SPM */ +#if defined(MBEDTLS_PSA_KEY_STORE_DYNAMIC) + "PSA_KEY_STORE_DYNAMIC", //no-check-names +#endif /* MBEDTLS_PSA_KEY_STORE_DYNAMIC */ #if defined(MBEDTLS_PSA_P256M_DRIVER_ENABLED) "PSA_P256M_DRIVER_ENABLED", //no-check-names #endif /* MBEDTLS_PSA_P256M_DRIVER_ENABLED */ diff --git a/thirdparty/mbedtls/library/x509_crt.c b/thirdparty/mbedtls/library/x509_crt.c index 2fd56fbd79..53cdcf0266 100644 --- a/thirdparty/mbedtls/library/x509_crt.c +++ b/thirdparty/mbedtls/library/x509_crt.c @@ -48,7 +48,9 @@ #if defined(MBEDTLS_HAVE_TIME) #if defined(_WIN32) && !defined(EFIX64) && !defined(EFI32) +#ifndef WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN +#endif #include <windows.h> #else #include <time.h> diff --git a/thirdparty/mbedtls/library/x509write_crt.c b/thirdparty/mbedtls/library/x509write_crt.c index 72f5a10a17..56f23c9fab 100644 --- a/thirdparty/mbedtls/library/x509write_crt.c +++ b/thirdparty/mbedtls/library/x509write_crt.c @@ -46,6 +46,10 @@ void mbedtls_x509write_crt_init(mbedtls_x509write_cert *ctx) void mbedtls_x509write_crt_free(mbedtls_x509write_cert *ctx) { + if (ctx == NULL) { + return; + } + mbedtls_asn1_free_named_data_list(&ctx->subject); mbedtls_asn1_free_named_data_list(&ctx->issuer); mbedtls_asn1_free_named_data_list(&ctx->extensions); diff --git a/thirdparty/mbedtls/library/x509write_csr.c b/thirdparty/mbedtls/library/x509write_csr.c index d3ddbcc03d..0d6f6bb1d3 100644 --- a/thirdparty/mbedtls/library/x509write_csr.c +++ b/thirdparty/mbedtls/library/x509write_csr.c @@ -43,6 +43,10 @@ void mbedtls_x509write_csr_init(mbedtls_x509write_csr *ctx) void mbedtls_x509write_csr_free(mbedtls_x509write_csr *ctx) { + if (ctx == NULL) { + return; + } + mbedtls_asn1_free_named_data_list(&ctx->subject); mbedtls_asn1_free_named_data_list(&ctx->extensions); diff --git a/thirdparty/mbedtls/patches/msvc-redeclaration-bug.diff b/thirdparty/mbedtls/patches/msvc-redeclaration-bug.diff index c5f1970223..3a15928fe1 100644 --- a/thirdparty/mbedtls/patches/msvc-redeclaration-bug.diff +++ b/thirdparty/mbedtls/patches/msvc-redeclaration-bug.diff @@ -1,5 +1,5 @@ diff --git a/thirdparty/mbedtls/include/psa/crypto.h b/thirdparty/mbedtls/include/psa/crypto.h -index 92f9c824e9..1cc2e7e729 100644 +index 2bbcea3ee0..96baf8f3ed 100644 --- a/thirdparty/mbedtls/include/psa/crypto.h +++ b/thirdparty/mbedtls/include/psa/crypto.h @@ -107,7 +107,9 @@ psa_status_t psa_crypto_init(void); @@ -12,7 +12,7 @@ index 92f9c824e9..1cc2e7e729 100644 /** Declare a key as persistent and set its key identifier. * -@@ -333,7 +335,9 @@ static void psa_set_key_bits(psa_key_attributes_t *attributes, +@@ -336,7 +338,9 @@ static void psa_set_key_bits(psa_key_attributes_t *attributes, * * \return The key type stored in the attribute structure. */ @@ -22,7 +22,7 @@ index 92f9c824e9..1cc2e7e729 100644 /** Retrieve the key size from key attributes. * -@@ -936,7 +940,9 @@ typedef struct psa_hash_operation_s psa_hash_operation_t; +@@ -939,7 +943,9 @@ typedef struct psa_hash_operation_s psa_hash_operation_t; /** Return an initial value for a hash operation object. */ @@ -32,7 +32,7 @@ index 92f9c824e9..1cc2e7e729 100644 /** Set up a multipart hash operation. * -@@ -1295,7 +1301,9 @@ typedef struct psa_mac_operation_s psa_mac_operation_t; +@@ -1298,7 +1304,9 @@ typedef struct psa_mac_operation_s psa_mac_operation_t; /** Return an initial value for a MAC operation object. */ @@ -42,7 +42,7 @@ index 92f9c824e9..1cc2e7e729 100644 /** Set up a multipart MAC calculation operation. * -@@ -1708,7 +1716,9 @@ typedef struct psa_cipher_operation_s psa_cipher_operation_t; +@@ -1711,7 +1719,9 @@ typedef struct psa_cipher_operation_s psa_cipher_operation_t; /** Return an initial value for a cipher operation object. */ @@ -52,7 +52,7 @@ index 92f9c824e9..1cc2e7e729 100644 /** Set the key for a multipart symmetric encryption operation. * -@@ -2226,7 +2236,9 @@ typedef struct psa_aead_operation_s psa_aead_operation_t; +@@ -2229,7 +2239,9 @@ typedef struct psa_aead_operation_s psa_aead_operation_t; /** Return an initial value for an AEAD operation object. */ @@ -62,7 +62,7 @@ index 92f9c824e9..1cc2e7e729 100644 /** Set the key for a multipart authenticated encryption operation. * -@@ -3213,7 +3225,9 @@ typedef struct psa_key_derivation_s psa_key_derivation_operation_t; +@@ -3216,7 +3228,9 @@ typedef struct psa_key_derivation_s psa_key_derivation_operation_t; /** Return an initial value for a key derivation operation object. */ @@ -73,10 +73,10 @@ index 92f9c824e9..1cc2e7e729 100644 /** Set up a key derivation operation. * diff --git a/thirdparty/mbedtls/include/psa/crypto_extra.h b/thirdparty/mbedtls/include/psa/crypto_extra.h -index 6ed1f6c43a..2686b9d74d 100644 +index 0cf42c6055..d276cd4c7f 100644 --- a/thirdparty/mbedtls/include/psa/crypto_extra.h +++ b/thirdparty/mbedtls/include/psa/crypto_extra.h -@@ -915,7 +915,9 @@ typedef struct psa_pake_cipher_suite_s psa_pake_cipher_suite_t; +@@ -923,7 +923,9 @@ typedef struct psa_pake_cipher_suite_s psa_pake_cipher_suite_t; /** Return an initial value for a PAKE cipher suite object. */ @@ -86,7 +86,7 @@ index 6ed1f6c43a..2686b9d74d 100644 /** Retrieve the PAKE algorithm from a PAKE cipher suite. * -@@ -1048,7 +1050,9 @@ typedef struct psa_jpake_computation_stage_s psa_jpake_computation_stage_t; +@@ -1056,7 +1058,9 @@ typedef struct psa_jpake_computation_stage_s psa_jpake_computation_stage_t; /** Return an initial value for a PAKE operation object. */ diff --git a/thirdparty/mbedtls/patches/no-flexible-arrays.diff b/thirdparty/mbedtls/patches/no-flexible-arrays.diff deleted file mode 100644 index 87fd06f1e3..0000000000 --- a/thirdparty/mbedtls/patches/no-flexible-arrays.diff +++ /dev/null @@ -1,132 +0,0 @@ -diff --git a/thirdparty/mbedtls/include/psa/crypto.h b/thirdparty/mbedtls/include/psa/crypto.h -index 7083bd911b..92f9c824e9 100644 ---- a/thirdparty/mbedtls/include/psa/crypto.h -+++ b/thirdparty/mbedtls/include/psa/crypto.h -@@ -3834,12 +3834,14 @@ psa_status_t psa_key_derivation_output_key( - * It is implementation-dependent whether a failure to initialize - * results in this error code. - */ -+#ifndef __cplusplus - psa_status_t psa_key_derivation_output_key_ext( - const psa_key_attributes_t *attributes, - psa_key_derivation_operation_t *operation, - const psa_key_production_parameters_t *params, - size_t params_data_length, - mbedtls_svc_key_id_t *key); -+#endif - - /** Compare output data from a key derivation operation to an expected value. - * -@@ -4180,10 +4182,12 @@ psa_status_t psa_generate_key(const psa_key_attributes_t *attributes, - * It is implementation-dependent whether a failure to initialize - * results in this error code. - */ -+#ifndef __cplusplus - psa_status_t psa_generate_key_ext(const psa_key_attributes_t *attributes, - const psa_key_production_parameters_t *params, - size_t params_data_length, - mbedtls_svc_key_id_t *key); -+#endif - - /**@}*/ - -diff --git a/thirdparty/mbedtls/include/psa/crypto_struct.h b/thirdparty/mbedtls/include/psa/crypto_struct.h -index 3913551aa8..e2c227b2eb 100644 ---- a/thirdparty/mbedtls/include/psa/crypto_struct.h -+++ b/thirdparty/mbedtls/include/psa/crypto_struct.h -@@ -223,11 +223,13 @@ static inline struct psa_key_derivation_s psa_key_derivation_operation_init( - return v; - } - -+#ifndef __cplusplus - struct psa_key_production_parameters_s { - /* Future versions may add other fields in this structure. */ - uint32_t flags; - uint8_t data[]; - }; -+#endif - - /** The default production parameters for key generation or key derivation. - * -diff --git a/thirdparty/mbedtls/include/psa/crypto_types.h b/thirdparty/mbedtls/include/psa/crypto_types.h -index c21bad86cc..a36b6ee65d 100644 ---- a/thirdparty/mbedtls/include/psa/crypto_types.h -+++ b/thirdparty/mbedtls/include/psa/crypto_types.h -@@ -477,7 +477,9 @@ typedef uint16_t psa_key_derivation_step_t; - * - Other key types: reserved for future use. \c flags must be 0. - * - */ -+#ifndef __cplusplus - typedef struct psa_key_production_parameters_s psa_key_production_parameters_t; -+#endif - - /**@}*/ - -diff --git a/thirdparty/mbedtls/library/psa_crypto_core.h b/thirdparty/mbedtls/library/psa_crypto_core.h -index 9462d2e8be..c059162efe 100644 ---- a/thirdparty/mbedtls/library/psa_crypto_core.h -+++ b/thirdparty/mbedtls/library/psa_crypto_core.h -@@ -351,9 +351,11 @@ psa_status_t psa_export_public_key_internal( - * \param[in] params The key production parameters to check. - * \param params_data_length Size of `params->data` in bytes. - */ -+#ifndef __cplusplus - int psa_key_production_parameters_are_default( - const psa_key_production_parameters_t *params, - size_t params_data_length); -+#endif - - /** - * \brief Generate a key. -@@ -378,12 +380,14 @@ int psa_key_production_parameters_are_default( - * \retval #PSA_ERROR_BUFFER_TOO_SMALL - * The size of \p key_buffer is too small. - */ -+#ifndef __cplusplus - psa_status_t psa_generate_key_internal(const psa_key_attributes_t *attributes, - const psa_key_production_parameters_t *params, - size_t params_data_length, - uint8_t *key_buffer, - size_t key_buffer_size, - size_t *key_buffer_length); -+#endif - - /** Sign a message with a private key. For hash-and-sign algorithms, - * this includes the hashing step. -diff --git a/thirdparty/mbedtls/library/psa_crypto_driver_wrappers.h b/thirdparty/mbedtls/library/psa_crypto_driver_wrappers.h -index ea6aee32eb..6919971aca 100644 ---- a/thirdparty/mbedtls/library/psa_crypto_driver_wrappers.h -+++ b/thirdparty/mbedtls/library/psa_crypto_driver_wrappers.h -@@ -728,6 +728,7 @@ static inline psa_status_t psa_driver_wrapper_get_key_buffer_size_from_key_data( - } - } - -+#ifndef __cplusplus - static inline psa_status_t psa_driver_wrapper_generate_key( - const psa_key_attributes_t *attributes, - const psa_key_production_parameters_t *params, size_t params_data_length, -@@ -832,6 +833,7 @@ static inline psa_status_t psa_driver_wrapper_generate_key( - - return( status ); - } -+#endif - - static inline psa_status_t psa_driver_wrapper_import_key( - const psa_key_attributes_t *attributes, -diff --git a/thirdparty/mbedtls/library/psa_crypto_rsa.h b/thirdparty/mbedtls/library/psa_crypto_rsa.h -index ffeef26be1..6d695ddf50 100644 ---- a/thirdparty/mbedtls/library/psa_crypto_rsa.h -+++ b/thirdparty/mbedtls/library/psa_crypto_rsa.h -@@ -130,10 +130,12 @@ psa_status_t mbedtls_psa_rsa_export_public_key( - * \retval #PSA_ERROR_BUFFER_TOO_SMALL - * The size of \p key_buffer is too small. - */ -+#ifndef __cplusplus - psa_status_t mbedtls_psa_rsa_generate_key( - const psa_key_attributes_t *attributes, - const psa_key_production_parameters_t *params, size_t params_data_length, - uint8_t *key_buffer, size_t key_buffer_size, size_t *key_buffer_length); -+#endif - - /** Sign an already-calculated hash with an RSA private key. - * diff --git a/thirdparty/misc/patches/qoa-min-fix.patch b/thirdparty/misc/patches/qoa-min-fix.patch index 38303a1521..6008b5f8bc 100644 --- a/thirdparty/misc/patches/qoa-min-fix.patch +++ b/thirdparty/misc/patches/qoa-min-fix.patch @@ -1,5 +1,5 @@ diff --git a/qoa.h b/qoa.h -index 592082933a..c890b88bd6 100644 +index cfed266bef..23612bb0bf 100644 --- a/qoa.h +++ b/qoa.h @@ -140,14 +140,14 @@ typedef struct { @@ -24,19 +24,15 @@ index 592082933a..c890b88bd6 100644 #ifndef QOA_NO_STDIO -@@ -394,9 +394,9 @@ unsigned int qoa_encode_frame(const short *sample_data, qoa_desc *qoa, unsigned - #ifdef QOA_RECORD_TOTAL_ERROR +@@ -395,7 +395,7 @@ unsigned int qoa_encode_frame(const short *sample_data, qoa_desc *qoa, unsigned qoa_uint64_t best_error = -1; #endif -- qoa_uint64_t best_slice; + qoa_uint64_t best_slice = 0; - qoa_lms_t best_lms; -- int best_scalefactor; -+ qoa_uint64_t best_slice = -1; -+ qoa_lms_t best_lms = {{-1, -1, -1, -1}, {-1, -1, -1, -1}}; -+ int best_scalefactor = -1; ++ qoa_lms_t best_lms = {}; + int best_scalefactor = 0; for (int sfi = 0; sfi < 16; sfi++) { - /* There is a strong correlation between the scalefactors of @@ -500,7 +500,7 @@ void *qoa_encode(const short *sample_data, qoa_desc *qoa, unsigned int *out_len) num_frames * QOA_LMS_LEN * 4 * qoa->channels + /* 4 * 4 bytes lms state per channel */ num_slices * 8 * qoa->channels; /* 8 byte slices */ diff --git a/thirdparty/misc/qoa.h b/thirdparty/misc/qoa.h index c890b88bd6..23612bb0bf 100644 --- a/thirdparty/misc/qoa.h +++ b/thirdparty/misc/qoa.h @@ -394,9 +394,9 @@ unsigned int qoa_encode_frame(const short *sample_data, qoa_desc *qoa, unsigned #ifdef QOA_RECORD_TOTAL_ERROR qoa_uint64_t best_error = -1; #endif - qoa_uint64_t best_slice = -1; - qoa_lms_t best_lms = {{-1, -1, -1, -1}, {-1, -1, -1, -1}}; - int best_scalefactor = -1; + qoa_uint64_t best_slice = 0; + qoa_lms_t best_lms = {}; + int best_scalefactor = 0; for (int sfi = 0; sfi < 16; sfi++) { /* There is a strong correlation between the scalefactors of diff --git a/thirdparty/thorvg/inc/config.h b/thirdparty/thorvg/inc/config.h index 8c185ccbca..02fec07448 100644 --- a/thirdparty/thorvg/inc/config.h +++ b/thirdparty/thorvg/inc/config.h @@ -15,5 +15,5 @@ // For internal debugging: //#define THORVG_LOG_ENABLED -#define THORVG_VERSION_STRING "0.14.2" +#define THORVG_VERSION_STRING "0.14.8" #endif diff --git a/thirdparty/thorvg/inc/thorvg.h b/thirdparty/thorvg/inc/thorvg.h index 47414d851a..4303092a5e 100644 --- a/thirdparty/thorvg/inc/thorvg.h +++ b/thirdparty/thorvg/inc/thorvg.h @@ -157,7 +157,7 @@ enum class FillRule enum class CompositeMethod { None = 0, ///< No composition is applied. - ClipPath, ///< The intersection of the source and the target is determined and only the resulting pixels from the source are rendered. + ClipPath, ///< The intersection of the source and the target is determined and only the resulting pixels from the source are rendered. Note that ClipPath only supports the Shape type. AlphaMask, ///< Alpha Masking using the compositing target's pixels as an alpha value. InvAlphaMask, ///< Alpha Masking using the complement to the compositing target's pixels as an alpha value. LumaMask, ///< Alpha Masking using the grayscale (0.2125R + 0.7154G + 0.0721*B) of the compositing target's pixels. @since 0.9 @@ -165,7 +165,9 @@ enum class CompositeMethod AddMask, ///< Combines the target and source objects pixels using target alpha. (T * TA) + (S * (255 - TA)) (Experimental API) SubtractMask, ///< Subtracts the source color from the target color while considering their respective target alpha. (T * TA) - (S * (255 - TA)) (Experimental API) IntersectMask, ///< Computes the result by taking the minimum value between the target alpha and the source alpha and multiplies it with the target color. (T * min(TA, SA)) (Experimental API) - DifferenceMask ///< Calculates the absolute difference between the target color and the source color multiplied by the complement of the target alpha. abs(T - S * (255 - TA)) (Experimental API) + DifferenceMask, ///< Calculates the absolute difference between the target color and the source color multiplied by the complement of the target alpha. abs(T - S * (255 - TA)) (Experimental API) + LightenMask, ///< Where multiple masks intersect, the highest transparency value is used. (Experimental API) + DarkenMask ///< Where multiple masks intersect, the lowest transparency value is used. (Experimental API) }; @@ -233,34 +235,6 @@ struct Matrix /** - * @brief A data structure representing a texture mesh vertex - * - * @param pt The vertex coordinate - * @param uv The normalized texture coordinate in the range (0.0..1.0, 0.0..1.0) - * - * @note Experimental API - */ -struct Vertex -{ - Point pt; - Point uv; -}; - - -/** - * @brief A data structure representing a triange in a texture mesh - * - * @param vertex The three vertices that make up the polygon - * - * @note Experimental API - */ -struct Polygon -{ - Vertex vertex[3]; -}; - - -/** * @class Paint * * @brief An abstract class for managing graphical elements. @@ -361,7 +335,7 @@ public: * * @note Experimental API */ - Result blend(BlendMethod method) const noexcept; + Result blend(BlendMethod method) noexcept; /** * @deprecated Use bounds(float* x, float* y, float* w, float* h, bool transformed) instead @@ -371,15 +345,16 @@ public: /** * @brief Gets the axis-aligned bounding box of the paint object. * - * In case @p transform is @c true, all object's transformations are applied first, and then the bounding box is established. Otherwise, the bounding box is determined before any transformations. - * - * @param[out] x The x coordinate of the upper left corner of the object. - * @param[out] y The y coordinate of the upper left corner of the object. + * @param[out] x The x-coordinate of the upper-left corner of the object. + * @param[out] y The y-coordinate of the upper-left corner of the object. * @param[out] w The width of the object. * @param[out] h The height of the object. - * @param[in] transformed If @c true, the paint's transformations are taken into account, otherwise they aren't. + * @param[in] transformed If @c true, the paint's transformations are taken into account in the scene it belongs to. Otherwise they aren't. * + * @note This is useful when you need to figure out the bounding box of the paint in the canvas space. * @note The bounding box doesn't indicate the actual drawing region. It's the smallest rectangle that encloses the object. + * @note If @p transformed is @c true, the paint needs to be pushed into a canvas and updated before this api is called. + * @see Canvas::update() */ Result bounds(float* x, float* y, float* w, float* h, bool transformed) const noexcept; @@ -411,9 +386,9 @@ public: CompositeMethod composite(const Paint** target) const noexcept; /** - * @brief Gets the blending method of the object. + * @brief Retrieves the current blending method applied to the paint object. * - * @return The blending method + * @return The currently set blending method. * * @note Experimental API */ @@ -428,6 +403,15 @@ public: */ uint32_t identifier() const noexcept; + /** + * @brief Unique ID of this instance. + * + * This is reserved to specify an paint instance in a scene. + * + * @since Experimental API + */ + uint32_t id = 0; + _TVG_DECLARE_PRIVATE(Paint); }; @@ -675,7 +659,8 @@ public: * @param[in] x2 The horizontal coordinate of the second point used to determine the gradient bounds. * @param[in] y2 The vertical coordinate of the second point used to determine the gradient bounds. * - * @note In case the first and the second points are equal, an object filled with such a gradient fill is not rendered. + * @note In case the first and the second points are equal, an object is filled with a single color using the last color specified in the colorStops(). + * @see Fill::colorStops() */ Result linear(float x1, float y1, float x2, float y2) noexcept; @@ -734,6 +719,8 @@ public: * @param[in] radius The radius of the bounding circle. * * @retval Result::InvalidArguments in case the @p radius value is zero or less. + * + * @note In case the @p radius is zero, an object is filled with a single color using the last color specified in the colorStops(). */ Result radial(float cx, float cy, float radius) noexcept; @@ -990,7 +977,7 @@ public: /** * @brief Sets the trim of the stroke along the defined path segment, allowing control over which part of the stroke is visible. * - * The values of the arguments @p begin, @p end, and @p offset are in the range of 0.0 to 1.0, representing the beginning of the path and the end, respectively. + * If the values of the arguments @p begin and @p end exceed the 0-1 range, they are wrapped around in a manner similar to angle wrapping, effectively treating the range as circular. * * @param[in] begin Specifies the start of the segment to display along the path. * @param[in] end Specifies the end of the segment to display along the path. @@ -1076,7 +1063,6 @@ public: * @param[out] b The blue color channel value in the range [0 ~ 255]. * @param[out] a The alpha channel value in the range [0 ~ 255], where 0 is completely transparent and 255 is opaque. * - * @return Result::Success when succeed. */ Result fillColor(uint8_t* r, uint8_t* g, uint8_t* b, uint8_t* a = nullptr) const noexcept; @@ -1219,7 +1205,7 @@ public: * when the @p copy has @c false. This means that loading the same data again will not result in duplicate operations * for the sharable @p data. Instead, ThorVG will reuse the previously loaded picture data. * - * @param[in] data A pointer to a memory location where the content of the picture file is stored. + * @param[in] data A pointer to a memory location where the content of the picture file is stored. A null-terminated string is expected for non-binary data if @p copy is @c false. * @param[in] size The size in bytes of the memory occupied by the @p data. * @param[in] mimeType Mimetype or extension of data such as "jpg", "jpeg", "lottie", "svg", "svg+xml", "png", etc. In case an empty string or an unknown type is provided, the loaders will be tried one by one. * @param[in] copy If @c true the data are copied into the engine local buffer, otherwise they are not. @@ -1256,7 +1242,7 @@ public: Result size(float* w, float* h) const noexcept; /** - * @brief Loads a raw data from a memory block with a given size. + * @brief Loads raw data in ARGB8888 format from a memory block of the given size. * * ThorVG efficiently caches the loaded data using the specified @p data address as a key * when the @p copy has @c false. This means that loading the same data again will not result in duplicate operations @@ -1265,47 +1251,27 @@ public: * @param[in] data A pointer to a memory location where the content of the picture raw data is stored. * @param[in] w The width of the image @p data in pixels. * @param[in] h The height of the image @p data in pixels. - * @param[in] premultiplied If @c true, the given image data is alpha-premultiplied. * @param[in] copy If @c true the data are copied into the engine local buffer, otherwise they are not. * + * @note It expects premultiplied alpha data. * @since 0.9 */ Result load(uint32_t* data, uint32_t w, uint32_t h, bool copy) noexcept; /** - * @brief Sets or removes the triangle mesh to deform the image. - * - * If a mesh is provided, the transform property of the Picture will apply to the triangle mesh, and the - * image data will be used as the texture. - * - * If @p triangles is @c nullptr, or @p triangleCnt is 0, the mesh will be removed. - * - * Only raster image types are supported at this time (png, jpg). Vector types like svg and tvg do not support. - * mesh deformation. However, if required you should be able to render a vector image to a raster image and then apply a mesh. + * @brief Retrieve a paint object from the Picture scene by its Unique ID. * - * @param[in] triangles An array of Polygons(triangles) that make up the mesh, or null to remove the mesh. - * @param[in] triangleCnt The number of Polygons(triangles) provided, or 0 to remove the mesh. + * This function searches for a paint object within the Picture scene that matches the provided @p id. * - * @note The Polygons are copied internally, so modifying them after calling Mesh::mesh has no affect. - * @warning Please do not use it, this API is not official one. It could be modified in the next version. + * @param[in] id The Unique ID of the paint object. * - * @note Experimental API - */ - Result mesh(const Polygon* triangles, uint32_t triangleCnt) noexcept; - - /** - * @brief Return the number of triangles in the mesh, and optionally get a pointer to the array of triangles in the mesh. + * @return A pointer to the paint object that matches the given identifier, or @c nullptr if no matching paint object is found. * - * @param[out] triangles Optional. A pointer to the array of Polygons used by this mesh. - * - * @return The number of polygons in the array. - * - * @note Modifying the triangles returned by this method will modify them directly within the mesh. - * @warning Please do not use it, this API is not official one. It could be modified in the next version. + * @see Accessor::id() * * @note Experimental API */ - uint32_t mesh(const Polygon** triangles) const noexcept; + const Paint* paint(uint32_t id) noexcept; /** * @brief Creates a new Picture object. @@ -1454,8 +1420,6 @@ public: * @param[in] g The green color channel value in the range [0 ~ 255]. The default value is 0. * @param[in] b The blue color channel value in the range [0 ~ 255]. The default value is 0. * - * @retval Result::InsufficientCondition when the font has not been set up prior to this operation. - * * @see Text::font() * * @note Experimental API @@ -1469,8 +1433,6 @@ public: * * @param[in] f The unique pointer to the gradient fill. * - * @retval Result::InsufficientCondition when the font has not been set up prior to this operation. - * * @note Either a solid color or a gradient fill is applied, depending on what was set as last. * @note Experimental API * @@ -1781,6 +1743,19 @@ public: */ static Result term(CanvasEngine engine) noexcept; + /** + * @brief Retrieves the version of the TVG engine. + * + * @param[out] major A major version number. + * @param[out] minor A minor version number. + * @param[out] micro A micro version number. + * + * @return The version of the engine in the format major.minor.micro, or a @p nullptr in case of an internal error. + * + * @note Experimental API + */ + static const char* version(uint32_t* major, uint32_t* minor, uint32_t* micro) noexcept; + _TVG_DISABLE_CTOR(Initializer); }; @@ -1879,7 +1854,7 @@ public: * @retval Result::InsufficientCondition In case the animation is not loaded. * @retval Result::NonSupport When it's not animatable. * - * @note Range from 0.0~1.0 + * @note Animation allows a range from 0.0 to 1.0. @p end should not be higher than @p begin. * @note If a marker has been specified, its range will be disregarded. * @see LottieAnimation::segment(const char* marker) * @note Experimental API @@ -2030,17 +2005,36 @@ class TVG_API Accessor final public: ~Accessor(); + TVG_DEPRECATED std::unique_ptr<Picture> set(std::unique_ptr<Picture> picture, std::function<bool(const Paint* paint)> func) noexcept; + /** * @brief Set the access function for traversing the Picture scene tree nodes. * * @param[in] picture The picture node to traverse the internal scene-tree. * @param[in] func The callback function calling for every paint nodes of the Picture. - * - * @return Return the given @p picture instance. + * @param[in] data Data passed to the @p func as its argument. * * @note The bitmap based picture might not have the scene-tree. + * + * @note Experimental API + */ + Result set(const Picture* picture, std::function<bool(const Paint* paint, void* data)> func, void* data) noexcept; + + /** + * @brief Generate a unique ID (hash key) from a given name. + * + * This function computes a unique identifier value based on the provided string. + * You can use this to assign a unique ID to the Paint object. + * + * @param[in] name The input string to generate the unique identifier from. + * + * @return The generated unique identifier value. + * + * @see Paint::id + * + * @note Experimental API */ - std::unique_ptr<Picture> set(std::unique_ptr<Picture> picture, std::function<bool(const Paint* paint)> func) noexcept; + static uint32_t id(const char* name) noexcept; /** * @brief Creates a new Accessor object. diff --git a/thirdparty/thorvg/patches/pr2702-sw_engine-handle-small-cubics.patch b/thirdparty/thorvg/patches/pr2702-sw_engine-handle-small-cubics.patch new file mode 100644 index 0000000000..69f4a5cf85 --- /dev/null +++ b/thirdparty/thorvg/patches/pr2702-sw_engine-handle-small-cubics.patch @@ -0,0 +1,96 @@ +From ac7d208ed8e4651c93ce1b2384070fccac9b6cb6 Mon Sep 17 00:00:00 2001 +From: Mira Grudzinska <mira@lottiefiles.com> +Date: Sun, 1 Sep 2024 22:36:18 +0200 +Subject: [PATCH] sw_engine: handle small cubics + +During the stroke's outline calculation, the function +handling small cubics set all angles to zero. Such cases +should be ignored, as further processing caused errors - +when the cubic was small but not zero, setting the angles +to zero resulted in incorrect outlines. + +@Issue: https://github.com/godotengine/godot/issues/96262 +--- + src/renderer/sw_engine/tvgSwCommon.h | 3 ++- + src/renderer/sw_engine/tvgSwMath.cpp | 19 ++++++++++++------- + src/renderer/sw_engine/tvgSwStroke.cpp | 16 +++++++++++----- + 3 files changed, 25 insertions(+), 13 deletions(-) + +diff --git a/src/renderer/sw_engine/tvgSwCommon.h b/src/renderer/sw_engine/tvgSwCommon.h +index 893e9beca..158fe8ecd 100644 +--- a/src/renderer/sw_engine/tvgSwCommon.h ++++ b/src/renderer/sw_engine/tvgSwCommon.h +@@ -491,7 +491,8 @@ SwFixed mathSin(SwFixed angle); + void mathSplitCubic(SwPoint* base); + SwFixed mathDiff(SwFixed angle1, SwFixed angle2); + SwFixed mathLength(const SwPoint& pt); +-bool mathSmallCubic(const SwPoint* base, SwFixed& angleIn, SwFixed& angleMid, SwFixed& angleOut); ++bool mathSmallCubic(const SwPoint* base); ++bool mathFlatCubic(const SwPoint* base, SwFixed& angleIn, SwFixed& angleMid, SwFixed& angleOut); + SwFixed mathMean(SwFixed angle1, SwFixed angle2); + SwPoint mathTransform(const Point* to, const Matrix& transform); + bool mathUpdateOutlineBBox(const SwOutline* outline, const SwBBox& clipRegion, SwBBox& renderRegion, bool fastTrack); +diff --git a/src/renderer/sw_engine/tvgSwMath.cpp b/src/renderer/sw_engine/tvgSwMath.cpp +index 1093edd62..b311be05f 100644 +--- a/src/renderer/sw_engine/tvgSwMath.cpp ++++ b/src/renderer/sw_engine/tvgSwMath.cpp +@@ -44,7 +44,17 @@ SwFixed mathMean(SwFixed angle1, SwFixed angle2) + } + + +-bool mathSmallCubic(const SwPoint* base, SwFixed& angleIn, SwFixed& angleMid, SwFixed& angleOut) ++bool mathSmallCubic(const SwPoint* base) ++{ ++ auto d1 = base[2] - base[3]; ++ auto d2 = base[1] - base[2]; ++ auto d3 = base[0] - base[1]; ++ ++ return d1.small() && d2.small() && d3.small(); ++} ++ ++ ++bool mathFlatCubic(const SwPoint* base, SwFixed& angleIn, SwFixed& angleMid, SwFixed& angleOut) + { + auto d1 = base[2] - base[3]; + auto d2 = base[1] - base[2]; +@@ -52,12 +62,7 @@ bool mathSmallCubic(const SwPoint* base, SwFixed& angleIn, SwFixed& angleMid, Sw + + if (d1.small()) { + if (d2.small()) { +- if (d3.small()) { +- angleIn = angleMid = angleOut = 0; +- return true; +- } else { +- angleIn = angleMid = angleOut = mathAtan(d3); +- } ++ angleIn = angleMid = angleOut = mathAtan(d3); + } else { + if (d3.small()) { + angleIn = angleMid = angleOut = mathAtan(d2); +diff --git a/src/renderer/sw_engine/tvgSwStroke.cpp b/src/renderer/sw_engine/tvgSwStroke.cpp +index 575d12951..4679b72cc 100644 +--- a/src/renderer/sw_engine/tvgSwStroke.cpp ++++ b/src/renderer/sw_engine/tvgSwStroke.cpp +@@ -441,11 +441,17 @@ static void _cubicTo(SwStroke& stroke, const SwPoint& ctrl1, const SwPoint& ctrl + //initialize with current direction + angleIn = angleOut = angleMid = stroke.angleIn; + +- if (arc < limit && !mathSmallCubic(arc, angleIn, angleMid, angleOut)) { +- if (stroke.firstPt) stroke.angleIn = angleIn; +- mathSplitCubic(arc); +- arc += 3; +- continue; ++ if (arc < limit) { ++ if (mathSmallCubic(arc)) { ++ arc -= 3; ++ continue; ++ } ++ if (!mathFlatCubic(arc, angleIn, angleMid, angleOut)) { ++ if (stroke.firstPt) stroke.angleIn = angleIn; ++ mathSplitCubic(arc); ++ arc += 3; ++ continue; ++ } + } + + if (firstArc) { diff --git a/thirdparty/thorvg/src/common/tvgArray.h b/thirdparty/thorvg/src/common/tvgArray.h index 8178bd0e42..19c8f69726 100644 --- a/thirdparty/thorvg/src/common/tvgArray.h +++ b/thirdparty/thorvg/src/common/tvgArray.h @@ -59,7 +59,7 @@ struct Array data[count++] = element; } - void push(Array<T>& rhs) + void push(const Array<T>& rhs) { if (rhs.count == 0) return; grow(rhs.count); diff --git a/thirdparty/thorvg/src/common/tvgCompressor.cpp b/thirdparty/thorvg/src/common/tvgCompressor.cpp index b61718f9a7..aebe9a4ef1 100644 --- a/thirdparty/thorvg/src/common/tvgCompressor.cpp +++ b/thirdparty/thorvg/src/common/tvgCompressor.cpp @@ -478,6 +478,8 @@ size_t b64Decode(const char* encoded, const size_t len, char** decoded) unsigned long djb2Encode(const char* str) { + if (!str) return 0; + unsigned long hash = 5381; int c; diff --git a/thirdparty/thorvg/src/common/tvgInlist.h b/thirdparty/thorvg/src/common/tvgInlist.h index ff28cfd48e..fc99ae3d14 100644 --- a/thirdparty/thorvg/src/common/tvgInlist.h +++ b/thirdparty/thorvg/src/common/tvgInlist.h @@ -100,7 +100,7 @@ struct Inlist if (element == tail) tail = element->prev; } - bool empty() + bool empty() const { return head ? false : true; } diff --git a/thirdparty/thorvg/src/common/tvgLines.cpp b/thirdparty/thorvg/src/common/tvgLines.cpp index 9d704900a5..49d992f127 100644 --- a/thirdparty/thorvg/src/common/tvgLines.cpp +++ b/thirdparty/thorvg/src/common/tvgLines.cpp @@ -79,7 +79,7 @@ float _bezAt(const Bezier& bz, float at, float length, LengthFunc lineLengthFunc Bezier left; bezSplitLeft(right, t, left); length = _bezLength(left, lineLengthFunc); - if (fabsf(length - at) < BEZIER_EPSILON || fabsf(smallest - biggest) < BEZIER_EPSILON) { + if (fabsf(length - at) < BEZIER_EPSILON || fabsf(smallest - biggest) < 1e-3f) { break; } if (length < at) { diff --git a/thirdparty/thorvg/src/common/tvgLock.h b/thirdparty/thorvg/src/common/tvgLock.h index 59f68d0d44..d3a4e41c5c 100644 --- a/thirdparty/thorvg/src/common/tvgLock.h +++ b/thirdparty/thorvg/src/common/tvgLock.h @@ -25,8 +25,6 @@ #ifdef THORVG_THREAD_SUPPORT -#define _DISABLE_CONSTEXPR_MUTEX_CONSTRUCTOR - #include <mutex> #include "tvgTaskScheduler.h" diff --git a/thirdparty/thorvg/src/common/tvgMath.cpp b/thirdparty/thorvg/src/common/tvgMath.cpp index c56b32249f..0254cce9b8 100644 --- a/thirdparty/thorvg/src/common/tvgMath.cpp +++ b/thirdparty/thorvg/src/common/tvgMath.cpp @@ -133,3 +133,11 @@ Point operator*(const Point& pt, const Matrix& m) auto ty = pt.x * m.e21 + pt.y * m.e22 + m.e23; return {tx, ty}; } + +uint8_t mathLerp(const uint8_t &start, const uint8_t &end, float t) +{ + auto result = static_cast<int>(start + (end - start) * t); + if (result > 255) result = 255; + else if (result < 0) result = 0; + return static_cast<uint8_t>(result); +} diff --git a/thirdparty/thorvg/src/common/tvgMath.h b/thirdparty/thorvg/src/common/tvgMath.h index 668260c689..df39e3b9af 100644 --- a/thirdparty/thorvg/src/common/tvgMath.h +++ b/thirdparty/thorvg/src/common/tvgMath.h @@ -78,17 +78,17 @@ bool mathIdentity(const Matrix* m); Matrix operator*(const Matrix& lhs, const Matrix& rhs); bool operator==(const Matrix& lhs, const Matrix& rhs); -static inline bool mathRightAngle(const Matrix* m) +static inline bool mathRightAngle(const Matrix& m) { - auto radian = fabsf(mathAtan2(m->e21, m->e11)); + auto radian = fabsf(mathAtan2(m.e21, m.e11)); if (radian < FLOAT_EPSILON || mathEqual(radian, MATH_PI2) || mathEqual(radian, MATH_PI)) return true; return false; } -static inline bool mathSkewed(const Matrix* m) +static inline bool mathSkewed(const Matrix& m) { - return !mathZero(m->e21 + m->e12); + return !mathZero(m.e21 + m.e12); } @@ -233,6 +233,17 @@ static inline Point operator/(const Point& lhs, const float rhs) } +static inline Point mathNormal(const Point& p1, const Point& p2) +{ + auto dir = p2 - p1; + auto len = mathLength(dir); + if (mathZero(len)) return {}; + + auto unitDir = dir / len; + return {-unitDir.y, unitDir.x}; +} + + static inline void mathLog(const Point& pt) { TVGLOG("COMMON", "Point: [%f %f]", pt.x, pt.y); @@ -248,5 +259,6 @@ static inline T mathLerp(const T &start, const T &end, float t) return static_cast<T>(start + (end - start) * t); } +uint8_t mathLerp(const uint8_t &start, const uint8_t &end, float t); #endif //_TVG_MATH_H_ diff --git a/thirdparty/thorvg/src/loaders/svg/tvgSvgLoader.cpp b/thirdparty/thorvg/src/loaders/svg/tvgSvgLoader.cpp index 8fbf3816ea..5aab4f1b0d 100644 --- a/thirdparty/thorvg/src/loaders/svg/tvgSvgLoader.cpp +++ b/thirdparty/thorvg/src/loaders/svg/tvgSvgLoader.cpp @@ -588,6 +588,11 @@ static bool _hslToRgb(float hue, float saturation, float brightness, uint8_t* re float _red = 0, _green = 0, _blue = 0; uint32_t i = 0; + while (hue < 0) hue += 360.0f; + hue = fmod(hue, 360.0f); + saturation = saturation > 0 ? std::min(saturation, 1.0f) : 0.0f; + brightness = brightness > 0 ? std::min(brightness, 1.0f) : 0.0f; + if (mathZero(saturation)) _red = _green = _blue = brightness; else { if (mathEqual(hue, 360.0)) hue = 0.0f; @@ -710,15 +715,15 @@ static bool _toColor(const char* str, uint8_t* r, uint8_t* g, uint8_t* b, char** return true; } else if (len >= 10 && (str[0] == 'h' || str[0] == 'H') && (str[1] == 's' || str[1] == 'S') && (str[2] == 'l' || str[2] == 'L') && str[3] == '(' && str[len - 1] == ')') { float th, ts, tb; - const char *content, *hue, *saturation, *brightness; - content = str + 4; - content = _skipSpace(content, nullptr); + const char* content = _skipSpace(str + 4, nullptr); + const char* hue = nullptr; if (_parseNumber(&content, &hue, &th) && hue) { - th = float(uint32_t(th) % 360); + const char* saturation = nullptr; hue = _skipSpace(hue, nullptr); hue = (char*)_skipComma(hue); hue = _skipSpace(hue, nullptr); if (_parseNumber(&hue, &saturation, &ts) && saturation && *saturation == '%') { + const char* brightness = nullptr; ts /= 100.0f; saturation = _skipSpace(saturation + 1, nullptr); saturation = (char*)_skipComma(saturation); diff --git a/thirdparty/thorvg/src/renderer/sw_engine/tvgSwCommon.h b/thirdparty/thorvg/src/renderer/sw_engine/tvgSwCommon.h index 05cbdc7f3a..09b75d370b 100644 --- a/thirdparty/thorvg/src/renderer/sw_engine/tvgSwCommon.h +++ b/thirdparty/thorvg/src/renderer/sw_engine/tvgSwCommon.h @@ -134,7 +134,6 @@ struct SwFill { struct SwLinear { float dx, dy; - float len; float offset; }; @@ -154,6 +153,7 @@ struct SwFill uint32_t* ctable; FillSpread spread; + bool solid = false; //solid color fill with the last color from colorStops bool translucent; }; @@ -301,8 +301,8 @@ static inline uint32_t JOIN(uint8_t c0, uint8_t c1, uint8_t c2, uint8_t c3) static inline uint32_t ALPHA_BLEND(uint32_t c, uint32_t a) { - return (((((c >> 8) & 0x00ff00ff) * a + 0x00ff00ff) & 0xff00ff00) + - ((((c & 0x00ff00ff) * a + 0x00ff00ff) >> 8) & 0x00ff00ff)); + ++a; + return (((((c >> 8) & 0x00ff00ff) * a) & 0xff00ff00) + ((((c & 0x00ff00ff) * a) >> 8) & 0x00ff00ff)); } static inline uint32_t INTERPOLATE(uint32_t s, uint32_t d, uint8_t a) @@ -367,7 +367,7 @@ static inline uint32_t opBlendSrcOver(uint32_t s, TVG_UNUSED uint32_t d, TVG_UNU } //TODO: BlendMethod could remove the alpha parameter. -static inline uint32_t opBlendDifference(uint32_t s, uint32_t d, TVG_UNUSED uint8_t a) +static inline uint32_t opBlendDifference(uint32_t s, uint32_t d, uint8_t a) { //if (s > d) => s - d //else => d - s @@ -404,8 +404,7 @@ static inline uint32_t opBlendScreen(uint32_t s, uint32_t d, TVG_UNUSED uint8_t return JOIN(255, c1, c2, c3); } - -static inline uint32_t opBlendMultiply(uint32_t s, uint32_t d, TVG_UNUSED uint8_t a) +static inline uint32_t opBlendDirectMultiply(uint32_t s, uint32_t d, uint8_t a) { // s * d auto c1 = MULTIPLY(C1(s), C1(d)); @@ -414,6 +413,10 @@ static inline uint32_t opBlendMultiply(uint32_t s, uint32_t d, TVG_UNUSED uint8_ return JOIN(255, c1, c2, c3); } +static inline uint32_t opBlendMultiply(uint32_t s, uint32_t d, uint8_t a) +{ + return opBlendDirectMultiply(s, d, a) + ALPHA_BLEND(d, IA(s)); +} static inline uint32_t opBlendOverlay(uint32_t s, uint32_t d, TVG_UNUSED uint8_t a) { @@ -492,40 +495,42 @@ SwFixed mathSin(SwFixed angle); void mathSplitCubic(SwPoint* base); SwFixed mathDiff(SwFixed angle1, SwFixed angle2); SwFixed mathLength(const SwPoint& pt); -bool mathSmallCubic(const SwPoint* base, SwFixed& angleIn, SwFixed& angleMid, SwFixed& angleOut); +bool mathSmallCubic(const SwPoint* base); +bool mathFlatCubic(const SwPoint* base, SwFixed& angleIn, SwFixed& angleMid, SwFixed& angleOut); SwFixed mathMean(SwFixed angle1, SwFixed angle2); -SwPoint mathTransform(const Point* to, const Matrix* transform); +SwPoint mathTransform(const Point* to, const Matrix& transform); bool mathUpdateOutlineBBox(const SwOutline* outline, const SwBBox& clipRegion, SwBBox& renderRegion, bool fastTrack); bool mathClipBBox(const SwBBox& clipper, SwBBox& clipee); void shapeReset(SwShape* shape); -bool shapePrepare(SwShape* shape, const RenderShape* rshape, const Matrix* transform, const SwBBox& clipRegion, SwBBox& renderRegion, SwMpool* mpool, unsigned tid, bool hasComposite); +bool shapePrepare(SwShape* shape, const RenderShape* rshape, const Matrix& transform, const SwBBox& clipRegion, SwBBox& renderRegion, SwMpool* mpool, unsigned tid, bool hasComposite); bool shapePrepared(const SwShape* shape); bool shapeGenRle(SwShape* shape, const RenderShape* rshape, bool antiAlias); void shapeDelOutline(SwShape* shape, SwMpool* mpool, uint32_t tid); -void shapeResetStroke(SwShape* shape, const RenderShape* rshape, const Matrix* transform); -bool shapeGenStrokeRle(SwShape* shape, const RenderShape* rshape, const Matrix* transform, const SwBBox& clipRegion, SwBBox& renderRegion, SwMpool* mpool, unsigned tid); +void shapeResetStroke(SwShape* shape, const RenderShape* rshape, const Matrix& transform); +bool shapeGenStrokeRle(SwShape* shape, const RenderShape* rshape, const Matrix& transform, const SwBBox& clipRegion, SwBBox& renderRegion, SwMpool* mpool, unsigned tid); void shapeFree(SwShape* shape); void shapeDelStroke(SwShape* shape); -bool shapeGenFillColors(SwShape* shape, const Fill* fill, const Matrix* transform, SwSurface* surface, uint8_t opacity, bool ctable); -bool shapeGenStrokeFillColors(SwShape* shape, const Fill* fill, const Matrix* transform, SwSurface* surface, uint8_t opacity, bool ctable); +bool shapeGenFillColors(SwShape* shape, const Fill* fill, const Matrix& transform, SwSurface* surface, uint8_t opacity, bool ctable); +bool shapeGenStrokeFillColors(SwShape* shape, const Fill* fill, const Matrix& transform, SwSurface* surface, uint8_t opacity, bool ctable); void shapeResetFill(SwShape* shape); void shapeResetStrokeFill(SwShape* shape); void shapeDelFill(SwShape* shape); void shapeDelStrokeFill(SwShape* shape); -void strokeReset(SwStroke* stroke, const RenderShape* shape, const Matrix* transform); +void strokeReset(SwStroke* stroke, const RenderShape* shape, const Matrix& transform); bool strokeParseOutline(SwStroke* stroke, const SwOutline& outline); SwOutline* strokeExportOutline(SwStroke* stroke, SwMpool* mpool, unsigned tid); void strokeFree(SwStroke* stroke); -bool imagePrepare(SwImage* image, const RenderMesh* mesh, const Matrix* transform, const SwBBox& clipRegion, SwBBox& renderRegion, SwMpool* mpool, unsigned tid); +bool imagePrepare(SwImage* image, const Matrix& transform, const SwBBox& clipRegion, SwBBox& renderRegion, SwMpool* mpool, unsigned tid); bool imageGenRle(SwImage* image, const SwBBox& renderRegion, bool antiAlias); void imageDelOutline(SwImage* image, SwMpool* mpool, uint32_t tid); void imageReset(SwImage* image); void imageFree(SwImage* image); -bool fillGenColorTable(SwFill* fill, const Fill* fdata, const Matrix* transform, SwSurface* surface, uint8_t opacity, bool ctable); +bool fillGenColorTable(SwFill* fill, const Fill* fdata, const Matrix& transform, SwSurface* surface, uint8_t opacity, bool ctable); +const Fill::ColorStop* fillFetchSolid(const SwFill* fill, const Fill* fdata); void fillReset(SwFill* fill); void fillFree(SwFill* fill); @@ -561,11 +566,11 @@ SwOutline* mpoolReqDashOutline(SwMpool* mpool, unsigned idx); void mpoolRetDashOutline(SwMpool* mpool, unsigned idx); bool rasterCompositor(SwSurface* surface); -bool rasterGradientShape(SwSurface* surface, SwShape* shape, unsigned id); +bool rasterGradientShape(SwSurface* surface, SwShape* shape, const Fill* fdata, uint8_t opacity); bool rasterShape(SwSurface* surface, SwShape* shape, uint8_t r, uint8_t g, uint8_t b, uint8_t a); -bool rasterImage(SwSurface* surface, SwImage* image, const RenderMesh* mesh, const Matrix* transform, const SwBBox& bbox, uint8_t opacity); +bool rasterImage(SwSurface* surface, SwImage* image, const Matrix& transform, const SwBBox& bbox, uint8_t opacity); bool rasterStroke(SwSurface* surface, SwShape* shape, uint8_t r, uint8_t g, uint8_t b, uint8_t a); -bool rasterGradientStroke(SwSurface* surface, SwShape* shape, unsigned id); +bool rasterGradientStroke(SwSurface* surface, SwShape* shape, const Fill* fdata, uint8_t opacity); bool rasterClear(SwSurface* surface, uint32_t x, uint32_t y, uint32_t w, uint32_t h); void rasterPixel32(uint32_t *dst, uint32_t val, uint32_t offset, int32_t len); void rasterGrayscale8(uint8_t *dst, uint8_t val, uint32_t offset, int32_t len); diff --git a/thirdparty/thorvg/src/renderer/sw_engine/tvgSwFill.cpp b/thirdparty/thorvg/src/renderer/sw_engine/tvgSwFill.cpp index bd0b5ffdcb..631294ad40 100644 --- a/thirdparty/thorvg/src/renderer/sw_engine/tvgSwFill.cpp +++ b/thirdparty/thorvg/src/renderer/sw_engine/tvgSwFill.cpp @@ -58,7 +58,7 @@ static void _calculateCoefficients(const SwFill* fill, uint32_t x, uint32_t y, f auto deltaDeltaRr = 2.0f * (radial->a11 * radial->a11 + radial->a21 * radial->a21) * radial->invA; det = b * b + (rr - radial->fr * radial->fr) * radial->invA; - deltaDet = 2.0f * b * deltaB + deltaB * deltaB + deltaRr + deltaDeltaRr; + deltaDet = 2.0f * b * deltaB + deltaB * deltaB + deltaRr + deltaDeltaRr * 0.5f; deltaDeltaDet = 2.0f * deltaB * deltaB + deltaDeltaRr; } @@ -125,6 +125,8 @@ static void _applyAA(const SwFill* fill, uint32_t begin, uint32_t end) static bool _updateColorTable(SwFill* fill, const Fill* fdata, const SwSurface* surface, uint8_t opacity) { + if (fill->solid) return true; + if (!fill->ctable) { fill->ctable = static_cast<uint32_t*>(malloc(GRADIENT_STOP_SIZE * sizeof(uint32_t))); if (!fill->ctable) return false; @@ -205,28 +207,33 @@ static bool _updateColorTable(SwFill* fill, const Fill* fdata, const SwSurface* } -bool _prepareLinear(SwFill* fill, const LinearGradient* linear, const Matrix* transform) +bool _prepareLinear(SwFill* fill, const LinearGradient* linear, const Matrix& transform) { float x1, x2, y1, y2; if (linear->linear(&x1, &y1, &x2, &y2) != Result::Success) return false; fill->linear.dx = x2 - x1; fill->linear.dy = y2 - y1; - fill->linear.len = fill->linear.dx * fill->linear.dx + fill->linear.dy * fill->linear.dy; + auto len = fill->linear.dx * fill->linear.dx + fill->linear.dy * fill->linear.dy; - if (fill->linear.len < FLOAT_EPSILON) return true; + if (len < FLOAT_EPSILON) { + if (mathZero(fill->linear.dx) && mathZero(fill->linear.dy)) { + fill->solid = true; + } + return true; + } - fill->linear.dx /= fill->linear.len; - fill->linear.dy /= fill->linear.len; + fill->linear.dx /= len; + fill->linear.dy /= len; fill->linear.offset = -fill->linear.dx * x1 - fill->linear.dy * y1; auto gradTransform = linear->transform(); bool isTransformation = !mathIdentity((const Matrix*)(&gradTransform)); if (isTransformation) { - if (transform) gradTransform = *transform * gradTransform; - } else if (transform) { - gradTransform = *transform; + gradTransform = transform * gradTransform; + } else { + gradTransform = transform; isTransformation = true; } @@ -239,15 +246,13 @@ bool _prepareLinear(SwFill* fill, const LinearGradient* linear, const Matrix* tr auto dx = fill->linear.dx; fill->linear.dx = dx * invTransform.e11 + fill->linear.dy * invTransform.e21; fill->linear.dy = dx * invTransform.e12 + fill->linear.dy * invTransform.e22; - - fill->linear.len = fill->linear.dx * fill->linear.dx + fill->linear.dy * fill->linear.dy; } return true; } -bool _prepareRadial(SwFill* fill, const RadialGradient* radial, const Matrix* transform) +bool _prepareRadial(SwFill* fill, const RadialGradient* radial, const Matrix& transform) { auto cx = P(radial)->cx; auto cy = P(radial)->cy; @@ -256,7 +261,10 @@ bool _prepareRadial(SwFill* fill, const RadialGradient* radial, const Matrix* tr auto fy = P(radial)->fy; auto fr = P(radial)->fr; - if (r < FLOAT_EPSILON) return true; + if (mathZero(r)) { + fill->solid = true; + return true; + } fill->radial.dr = r - fr; fill->radial.dx = cx - fx; @@ -289,12 +297,10 @@ bool _prepareRadial(SwFill* fill, const RadialGradient* radial, const Matrix* tr auto gradTransform = radial->transform(); bool isTransformation = !mathIdentity((const Matrix*)(&gradTransform)); - if (transform) { - if (isTransformation) gradTransform = *transform * gradTransform; - else { - gradTransform = *transform; - isTransformation = true; - } + if (isTransformation) gradTransform = transform * gradTransform; + else { + gradTransform = transform; + isTransformation = true; } if (isTransformation) { @@ -816,25 +822,32 @@ void fillLinear(const SwFill* fill, uint32_t* dst, uint32_t y, uint32_t x, uint3 } -bool fillGenColorTable(SwFill* fill, const Fill* fdata, const Matrix* transform, SwSurface* surface, uint8_t opacity, bool ctable) +bool fillGenColorTable(SwFill* fill, const Fill* fdata, const Matrix& transform, SwSurface* surface, uint8_t opacity, bool ctable) { if (!fill) return false; fill->spread = fdata->spread(); - if (ctable) { - if (!_updateColorTable(fill, fdata, surface, opacity)) return false; - } - if (fdata->identifier() == TVG_CLASS_ID_LINEAR) { - return _prepareLinear(fill, static_cast<const LinearGradient*>(fdata), transform); + if (!_prepareLinear(fill, static_cast<const LinearGradient*>(fdata), transform)) return false; } else if (fdata->identifier() == TVG_CLASS_ID_RADIAL) { - return _prepareRadial(fill, static_cast<const RadialGradient*>(fdata), transform); + if (!_prepareRadial(fill, static_cast<const RadialGradient*>(fdata), transform)) return false; } - //LOG: What type of gradient?! + if (ctable) return _updateColorTable(fill, fdata, surface, opacity); + return true; +} + + +const Fill::ColorStop* fillFetchSolid(const SwFill* fill, const Fill* fdata) +{ + if (!fill->solid) return nullptr; + + const Fill::ColorStop* colors; + auto cnt = fdata->colorStops(&colors); + if (cnt == 0 || !colors) return nullptr; - return false; + return colors + cnt - 1; } @@ -845,6 +858,7 @@ void fillReset(SwFill* fill) fill->ctable = nullptr; } fill->translucent = false; + fill->solid = false; } diff --git a/thirdparty/thorvg/src/renderer/sw_engine/tvgSwImage.cpp b/thirdparty/thorvg/src/renderer/sw_engine/tvgSwImage.cpp index e1d41a0d52..3fc64ce036 100644 --- a/thirdparty/thorvg/src/renderer/sw_engine/tvgSwImage.cpp +++ b/thirdparty/thorvg/src/renderer/sw_engine/tvgSwImage.cpp @@ -27,14 +27,14 @@ /* Internal Class Implementation */ /************************************************************************/ -static inline bool _onlyShifted(const Matrix* m) +static inline bool _onlyShifted(const Matrix& m) { - if (mathEqual(m->e11, 1.0f) && mathEqual(m->e22, 1.0f) && mathZero(m->e12) && mathZero(m->e21)) return true; + if (mathEqual(m.e11, 1.0f) && mathEqual(m.e22, 1.0f) && mathZero(m.e12) && mathZero(m.e21)) return true; return false; } -static bool _genOutline(SwImage* image, const RenderMesh* mesh, const Matrix* transform, SwMpool* mpool, unsigned tid) +static bool _genOutline(SwImage* image, const Matrix& transform, SwMpool* mpool, unsigned tid) { image->outline = mpoolReqOutline(mpool, tid); auto outline = image->outline; @@ -45,48 +45,12 @@ static bool _genOutline(SwImage* image, const RenderMesh* mesh, const Matrix* tr outline->closed.reserve(1); Point to[4]; - if (mesh->triangleCnt > 0) { - // TODO: Optimise me. We appear to calculate this exact min/max bounding area in multiple - // places. We should be able to re-use one we have already done? Also see: - // tvgPicture.h --> bounds - // tvgSwRasterTexmap.h --> _rasterTexmapPolygonMesh - // - // TODO: Should we calculate the exact path(s) of the triangle mesh instead? - // i.e. copy tvgSwShape.capp -> _genOutline? - // - // TODO: Cntrs? - auto triangles = mesh->triangles; - auto min = triangles[0].vertex[0].pt; - auto max = triangles[0].vertex[0].pt; - - for (uint32_t i = 0; i < mesh->triangleCnt; ++i) { - if (triangles[i].vertex[0].pt.x < min.x) min.x = triangles[i].vertex[0].pt.x; - else if (triangles[i].vertex[0].pt.x > max.x) max.x = triangles[i].vertex[0].pt.x; - if (triangles[i].vertex[0].pt.y < min.y) min.y = triangles[i].vertex[0].pt.y; - else if (triangles[i].vertex[0].pt.y > max.y) max.y = triangles[i].vertex[0].pt.y; - - if (triangles[i].vertex[1].pt.x < min.x) min.x = triangles[i].vertex[1].pt.x; - else if (triangles[i].vertex[1].pt.x > max.x) max.x = triangles[i].vertex[1].pt.x; - if (triangles[i].vertex[1].pt.y < min.y) min.y = triangles[i].vertex[1].pt.y; - else if (triangles[i].vertex[1].pt.y > max.y) max.y = triangles[i].vertex[1].pt.y; - - if (triangles[i].vertex[2].pt.x < min.x) min.x = triangles[i].vertex[2].pt.x; - else if (triangles[i].vertex[2].pt.x > max.x) max.x = triangles[i].vertex[2].pt.x; - if (triangles[i].vertex[2].pt.y < min.y) min.y = triangles[i].vertex[2].pt.y; - else if (triangles[i].vertex[2].pt.y > max.y) max.y = triangles[i].vertex[2].pt.y; - } - to[0] = {min.x, min.y}; - to[1] = {max.x, min.y}; - to[2] = {max.x, max.y}; - to[3] = {min.x, max.y}; - } else { - auto w = static_cast<float>(image->w); - auto h = static_cast<float>(image->h); - to[0] = {0, 0}; - to[1] = {w, 0}; - to[2] = {w, h}; - to[3] = {0, h}; - } + auto w = static_cast<float>(image->w); + auto h = static_cast<float>(image->h); + to[0] = {0, 0}; + to[1] = {w, 0}; + to[2] = {w, h}; + to[3] = {0, h}; for (int i = 0; i < 4; i++) { outline->pts.push(mathTransform(&to[i], transform)); @@ -108,25 +72,25 @@ static bool _genOutline(SwImage* image, const RenderMesh* mesh, const Matrix* tr /* External Class Implementation */ /************************************************************************/ -bool imagePrepare(SwImage* image, const RenderMesh* mesh, const Matrix* transform, const SwBBox& clipRegion, SwBBox& renderRegion, SwMpool* mpool, unsigned tid) +bool imagePrepare(SwImage* image, const Matrix& transform, const SwBBox& clipRegion, SwBBox& renderRegion, SwMpool* mpool, unsigned tid) { image->direct = _onlyShifted(transform); //Fast track: Non-transformed image but just shifted. if (image->direct) { - image->ox = -static_cast<int32_t>(nearbyint(transform->e13)); - image->oy = -static_cast<int32_t>(nearbyint(transform->e23)); + image->ox = -static_cast<int32_t>(nearbyint(transform.e13)); + image->oy = -static_cast<int32_t>(nearbyint(transform.e23)); //Figure out the scale factor by transform matrix } else { - auto scaleX = sqrtf((transform->e11 * transform->e11) + (transform->e21 * transform->e21)); - auto scaleY = sqrtf((transform->e22 * transform->e22) + (transform->e12 * transform->e12)); + auto scaleX = sqrtf((transform.e11 * transform.e11) + (transform.e21 * transform.e21)); + auto scaleY = sqrtf((transform.e22 * transform.e22) + (transform.e12 * transform.e12)); image->scale = (fabsf(scaleX - scaleY) > 0.01f) ? 1.0f : scaleX; - if (mathZero(transform->e12) && mathZero(transform->e21)) image->scaled = true; + if (mathZero(transform.e12) && mathZero(transform.e21)) image->scaled = true; else image->scaled = false; } - if (!_genOutline(image, mesh, transform, mpool, tid)) return false; + if (!_genOutline(image, transform, mpool, tid)) return false; return mathUpdateOutlineBBox(image->outline, clipRegion, renderRegion, image->direct); } diff --git a/thirdparty/thorvg/src/renderer/sw_engine/tvgSwMath.cpp b/thirdparty/thorvg/src/renderer/sw_engine/tvgSwMath.cpp index ae158c836a..60dbbc4fbc 100644 --- a/thirdparty/thorvg/src/renderer/sw_engine/tvgSwMath.cpp +++ b/thirdparty/thorvg/src/renderer/sw_engine/tvgSwMath.cpp @@ -44,7 +44,17 @@ SwFixed mathMean(SwFixed angle1, SwFixed angle2) } -bool mathSmallCubic(const SwPoint* base, SwFixed& angleIn, SwFixed& angleMid, SwFixed& angleOut) +bool mathSmallCubic(const SwPoint* base) +{ + auto d1 = base[2] - base[3]; + auto d2 = base[1] - base[2]; + auto d3 = base[0] - base[1]; + + return d1.small() && d2.small() && d3.small(); +} + + +bool mathFlatCubic(const SwPoint* base, SwFixed& angleIn, SwFixed& angleMid, SwFixed& angleOut) { auto d1 = base[2] - base[3]; auto d2 = base[1] - base[2]; @@ -52,12 +62,7 @@ bool mathSmallCubic(const SwPoint* base, SwFixed& angleIn, SwFixed& angleMid, Sw if (d1.small()) { if (d2.small()) { - if (d3.small()) { - angleIn = angleMid = angleOut = 0; - return true; - } else { - angleIn = angleMid = angleOut = mathAtan(d3); - } + angleIn = angleMid = angleOut = mathAtan(d3); } else { if (d3.small()) { angleIn = angleMid = angleOut = mathAtan(d2); @@ -254,12 +259,10 @@ SwFixed mathDiff(SwFixed angle1, SwFixed angle2) } -SwPoint mathTransform(const Point* to, const Matrix* transform) +SwPoint mathTransform(const Point* to, const Matrix& transform) { - if (!transform) return {TO_SWCOORD(to->x), TO_SWCOORD(to->y)}; - - auto tx = to->x * transform->e11 + to->y * transform->e12 + transform->e13; - auto ty = to->x * transform->e21 + to->y * transform->e22 + transform->e23; + auto tx = to->x * transform.e11 + to->y * transform.e12 + transform.e13; + auto ty = to->x * transform.e21 + to->y * transform.e22 + transform.e23; return {TO_SWCOORD(tx), TO_SWCOORD(ty)}; } diff --git a/thirdparty/thorvg/src/renderer/sw_engine/tvgSwRaster.cpp b/thirdparty/thorvg/src/renderer/sw_engine/tvgSwRaster.cpp index 042d1e2b44..90d91e17e0 100644 --- a/thirdparty/thorvg/src/renderer/sw_engine/tvgSwRaster.cpp +++ b/thirdparty/thorvg/src/renderer/sw_engine/tvgSwRaster.cpp @@ -194,10 +194,21 @@ static inline uint8_t _opMaskDifference(uint8_t s, uint8_t d, uint8_t a) } +static inline uint8_t _opMaskLighten(uint8_t s, uint8_t d, uint8_t a) +{ + return (s > d) ? s : d; +} + + +static inline uint8_t _opMaskDarken(uint8_t s, uint8_t d, uint8_t a) +{ + return (s < d) ? s : d; +} + + static inline bool _direct(CompositeMethod method) { - //subtract & Intersect allows the direct composition - if (method == CompositeMethod::SubtractMask || method == CompositeMethod::IntersectMask) return true; + if (method == CompositeMethod::SubtractMask || method == CompositeMethod::IntersectMask || method == CompositeMethod::DarkenMask) return true; return false; } @@ -209,6 +220,8 @@ static inline SwMask _getMaskOp(CompositeMethod method) case CompositeMethod::SubtractMask: return _opMaskSubtract; case CompositeMethod::DifferenceMask: return _opMaskDifference; case CompositeMethod::IntersectMask: return _opMaskIntersect; + case CompositeMethod::LightenMask: return _opMaskLighten; + case CompositeMethod::DarkenMask: return _opMaskDarken; default: return nullptr; } } @@ -832,7 +845,7 @@ static bool _rasterScaledRleImage(SwSurface* surface, const SwImage* image, cons } -static bool _scaledRleImage(SwSurface* surface, const SwImage* image, const Matrix* transform, const SwBBox& region, uint8_t opacity) +static bool _scaledRleImage(SwSurface* surface, const SwImage* image, const Matrix& transform, const SwBBox& region, uint8_t opacity) { if (surface->channelSize == sizeof(uint8_t)) { TVGERR("SW_ENGINE", "Not supported scaled rle image!"); @@ -841,9 +854,7 @@ static bool _scaledRleImage(SwSurface* surface, const SwImage* image, const Matr Matrix itransform; - if (transform) { - if (!mathInverse(transform, &itransform)) return false; - } else mathIdentity(&itransform); + if (!mathInverse(&transform, &itransform)) return true; if (_compositing(surface)) { if (_matting(surface)) return _rasterScaledMattedRleImage(surface, image, &itransform, region, opacity); @@ -1197,13 +1208,11 @@ static bool _rasterScaledImage(SwSurface* surface, const SwImage* image, const M } -static bool _scaledImage(SwSurface* surface, const SwImage* image, const Matrix* transform, const SwBBox& region, uint8_t opacity) +static bool _scaledImage(SwSurface* surface, const SwImage* image, const Matrix& transform, const SwBBox& region, uint8_t opacity) { Matrix itransform; - if (transform) { - if (!mathInverse(transform, &itransform)) return false; - } else mathIdentity(&itransform); + if (!mathInverse(&transform, &itransform)) return true; if (_compositing(surface)) { if (_matting(surface)) return _rasterScaledMattedImage(surface, image, &itransform, region, opacity); @@ -1374,7 +1383,7 @@ static bool _rasterDirectBlendingImage(SwSurface* surface, const SwImage* image, *dst = INTERPOLATE(tmp, *dst, A(*src)); } } else { - for (auto x = region.min.x; x < region.max.x; ++x, ++dst, ++src) { + for (auto x = region.min.x; x < region.max.x; x++, dst++, src++) { auto tmp = ALPHA_BLEND(*src, opacity); auto tmp2 = surface->blender(tmp, *dst, 255); *dst = INTERPOLATE(tmp2, *dst, A(tmp)); @@ -1389,29 +1398,45 @@ static bool _rasterDirectBlendingImage(SwSurface* surface, const SwImage* image, static bool _rasterDirectImage(SwSurface* surface, const SwImage* image, const SwBBox& region, uint8_t opacity) { - if (surface->channelSize == sizeof(uint8_t)) { - TVGERR("SW_ENGINE", "Not supported grayscale image!"); - return false; - } - - auto dbuffer = &surface->buf32[region.min.y * surface->stride + region.min.x]; auto sbuffer = image->buf32 + (region.min.y + image->oy) * image->stride + (region.min.x + image->ox); - for (auto y = region.min.y; y < region.max.y; ++y) { - auto dst = dbuffer; - auto src = sbuffer; - if (opacity == 255) { - for (auto x = region.min.x; x < region.max.x; x++, dst++, src++) { - *dst = *src + ALPHA_BLEND(*dst, IA(*src)); + //32bits channels + if (surface->channelSize == sizeof(uint32_t)) { + auto dbuffer = &surface->buf32[region.min.y * surface->stride + region.min.x]; + + for (auto y = region.min.y; y < region.max.y; ++y) { + auto dst = dbuffer; + auto src = sbuffer; + if (opacity == 255) { + for (auto x = region.min.x; x < region.max.x; x++, dst++, src++) { + *dst = *src + ALPHA_BLEND(*dst, IA(*src)); + } + } else { + for (auto x = region.min.x; x < region.max.x; ++x, ++dst, ++src) { + auto tmp = ALPHA_BLEND(*src, opacity); + *dst = tmp + ALPHA_BLEND(*dst, IA(tmp)); + } } - } else { - for (auto x = region.min.x; x < region.max.x; ++x, ++dst, ++src) { - auto tmp = ALPHA_BLEND(*src, opacity); - *dst = tmp + ALPHA_BLEND(*dst, IA(tmp)); + dbuffer += surface->stride; + sbuffer += image->stride; + } + //8bits grayscale + } else if (surface->channelSize == sizeof(uint8_t)) { + auto dbuffer = &surface->buf8[region.min.y * surface->stride + region.min.x]; + + for (auto y = region.min.y; y < region.max.y; ++y, dbuffer += surface->stride, sbuffer += image->stride) { + auto dst = dbuffer; + auto src = sbuffer; + if (opacity == 255) { + for (auto x = region.min.x; x < region.max.x; ++x, ++dst, ++src) { + *dst = *src + MULTIPLY(*dst, ~*src); + } + } else { + for (auto x = region.min.x; x < region.max.x; ++x, ++dst, ++src) { + *dst = INTERPOLATE8(*src, *dst, opacity); + } } } - dbuffer += surface->stride; - sbuffer += image->stride; } return true; } @@ -1433,7 +1458,7 @@ static bool _directImage(SwSurface* surface, const SwImage* image, const SwBBox& //Blenders for the following scenarios: [RLE / Whole] * [Direct / Scaled / Transformed] -static bool _rasterImage(SwSurface* surface, SwImage* image, const Matrix* transform, const SwBBox& region, uint8_t opacity) +static bool _rasterImage(SwSurface* surface, SwImage* image, const Matrix& transform, const SwBBox& region, uint8_t opacity) { //RLE Image if (image->rle) { @@ -1574,8 +1599,6 @@ static bool _rasterSolidGradientRect(SwSurface* surface, const SwBBox& region, c static bool _rasterLinearGradientRect(SwSurface* surface, const SwBBox& region, const SwFill* fill) { - if (fill->linear.len < FLOAT_EPSILON) return false; - if (_compositing(surface)) { if (_matting(surface)) return _rasterGradientMattedRect<FillLinear>(surface, region, fill); else return _rasterGradientMaskedRect<FillLinear>(surface, region, fill); @@ -1902,10 +1925,16 @@ void rasterPremultiply(Surface* surface) } -bool rasterGradientShape(SwSurface* surface, SwShape* shape, unsigned id) +bool rasterGradientShape(SwSurface* surface, SwShape* shape, const Fill* fdata, uint8_t opacity) { if (!shape->fill) return false; + if (auto color = fillFetchSolid(shape->fill, fdata)) { + auto a = MULTIPLY(color->a, opacity); + return a > 0 ? rasterShape(surface, shape, color->r, color->g, color->b, a) : true; + } + + auto id = fdata->identifier(); if (shape->fastTrack) { if (id == TVG_CLASS_ID_LINEAR) return _rasterLinearGradientRect(surface, shape->bbox, shape->fill); else if (id == TVG_CLASS_ID_RADIAL)return _rasterRadialGradientRect(surface, shape->bbox, shape->fill); @@ -1917,10 +1946,16 @@ bool rasterGradientShape(SwSurface* surface, SwShape* shape, unsigned id) } -bool rasterGradientStroke(SwSurface* surface, SwShape* shape, unsigned id) +bool rasterGradientStroke(SwSurface* surface, SwShape* shape, const Fill* fdata, uint8_t opacity) { if (!shape->stroke || !shape->stroke->fill || !shape->strokeRle) return false; + if (auto color = fillFetchSolid(shape->stroke->fill, fdata)) { + auto a = MULTIPLY(color->a, opacity); + return a > 0 ? rasterStroke(surface, shape, color->r, color->g, color->b, a) : true; + } + + auto id = fdata->identifier(); if (id == TVG_CLASS_ID_LINEAR) return _rasterLinearGradientRle(surface, shape->strokeRle, shape->stroke->fill); else if (id == TVG_CLASS_ID_RADIAL) return _rasterRadialGradientRle(surface, shape->strokeRle, shape->stroke->fill); @@ -1952,13 +1987,12 @@ bool rasterStroke(SwSurface* surface, SwShape* shape, uint8_t r, uint8_t g, uint } -bool rasterImage(SwSurface* surface, SwImage* image, const RenderMesh* mesh, const Matrix* transform, const SwBBox& bbox, uint8_t opacity) +bool rasterImage(SwSurface* surface, SwImage* image, const Matrix& transform, const SwBBox& bbox, uint8_t opacity) { //Outside of the viewport, skip the rendering if (bbox.max.x < 0 || bbox.max.y < 0 || bbox.min.x >= static_cast<SwCoord>(surface->w) || bbox.min.y >= static_cast<SwCoord>(surface->h)) return true; - if (mesh && mesh->triangleCnt > 0) return _rasterTexmapPolygonMesh(surface, image, mesh, transform, &bbox, opacity); - else return _rasterImage(surface, image, transform, bbox, opacity); + return _rasterImage(surface, image, transform, bbox, opacity); } diff --git a/thirdparty/thorvg/src/renderer/sw_engine/tvgSwRasterTexmap.h b/thirdparty/thorvg/src/renderer/sw_engine/tvgSwRasterTexmap.h index cfce7785c7..88ef2118f2 100644 --- a/thirdparty/thorvg/src/renderer/sw_engine/tvgSwRasterTexmap.h +++ b/thirdparty/thorvg/src/renderer/sw_engine/tvgSwRasterTexmap.h @@ -53,12 +53,10 @@ static bool _arrange(const SwImage* image, const SwBBox* region, int& yStart, in regionBottom = image->rle->spans[image->rle->size - 1].y; } - if (yStart >= regionBottom) return false; - if (yStart < regionTop) yStart = regionTop; if (yEnd > regionBottom) yEnd = regionBottom; - return true; + return yEnd > yStart; } @@ -868,10 +866,8 @@ static void _calcVertCoverage(AALine *lines, int32_t eidx, int32_t y, int32_t re static void _calcHorizCoverage(AALine *lines, int32_t eidx, int32_t y, int32_t x, int32_t x2) { - if (lines[y].length[eidx] < abs(x - x2)) { - lines[y].length[eidx] = abs(x - x2); - lines[y].coverage[eidx] = (255 / (lines[y].length[eidx] + 1)); - } + lines[y].length[eidx] = abs(x - x2); + lines[y].coverage[eidx] = (255 / (lines[y].length[eidx] + 1)); } @@ -897,9 +893,14 @@ static void _calcAAEdge(AASpans *aaSpans, int32_t eidx) ptx[1] = tx[1]; \ } while (0) + struct Point + { + int32_t x, y; + }; + int32_t y = 0; - SwPoint pEdge = {-1, -1}; //previous edge point - SwPoint edgeDiff = {0, 0}; //temporary used for point distance + Point pEdge = {-1, -1}; //previous edge point + Point edgeDiff = {0, 0}; //temporary used for point distance /* store bigger to tx[0] between prev and current edge's x positions. */ int32_t tx[2] = {0, 0}; @@ -1024,6 +1025,7 @@ static void _calcAAEdge(AASpans *aaSpans, int32_t eidx) static bool _apply(SwSurface* surface, AASpans* aaSpans) { + auto end = surface->buf32 + surface->h * surface->stride; auto y = aaSpans->yStart; uint32_t pixel; uint32_t* dst; @@ -1044,8 +1046,13 @@ static bool _apply(SwSurface* surface, AASpans* aaSpans) dst = surface->buf32 + (offset + line->x[0]); if (line->x[0] > 1) pixel = *(dst - 1); else pixel = *dst; - pos = 1; + + //exceptional handling. out of memory bound. + if (dst + line->length[0] >= end) { + pos += (dst + line->length[0] - end); + } + while (pos <= line->length[0]) { *dst = INTERPOLATE(*dst, pixel, line->coverage[0] * pos); ++dst; @@ -1053,17 +1060,21 @@ static bool _apply(SwSurface* surface, AASpans* aaSpans) } //Right edge - dst = surface->buf32 + (offset + line->x[1] - 1); + dst = surface->buf32 + offset + line->x[1] - 1; + if (line->x[1] < (int32_t)(surface->w - 1)) pixel = *(dst + 1); else pixel = *dst; + pos = line->length[1]; - pos = width; - while ((int32_t)(width - line->length[1]) < pos) { - *dst = INTERPOLATE(*dst, pixel, 255 - (line->coverage[1] * (line->length[1] - (width - pos)))); + //exceptional handling. out of memory bound. + if (dst - pos < surface->buf32) --pos; + + while (pos > 0) { + *dst = INTERPOLATE(*dst, pixel, 255 - (line->coverage[1] * pos)); --dst; --pos; } - } + } y++; } @@ -1084,7 +1095,7 @@ static bool _apply(SwSurface* surface, AASpans* aaSpans) | / | 3 -- 2 */ -static bool _rasterTexmapPolygon(SwSurface* surface, const SwImage* image, const Matrix* transform, const SwBBox* region, uint8_t opacity) +static bool _rasterTexmapPolygon(SwSurface* surface, const SwImage* image, const Matrix& transform, const SwBBox* region, uint8_t opacity) { if (surface->channelSize == sizeof(uint8_t)) { TVGERR("SW_ENGINE", "Not supported grayscale Textmap polygon!"); @@ -1092,7 +1103,7 @@ static bool _rasterTexmapPolygon(SwSurface* surface, const SwImage* image, const } //Exceptions: No dedicated drawing area? - if ((!image->rle && !region) || (image->rle && image->rle->size == 0)) return false; + if ((!image->rle && !region) || (image->rle && image->rle->size == 0)) return true; /* Prepare vertices. shift XY coordinates to match the sub-pixeling technique. */ @@ -1104,7 +1115,7 @@ static bool _rasterTexmapPolygon(SwSurface* surface, const SwImage* image, const float ys = FLT_MAX, ye = -1.0f; for (int i = 0; i < 4; i++) { - if (transform) vertices[i].pt *= *transform; + vertices[i].pt *= transform; if (vertices[i].pt.y < ys) ys = vertices[i].pt.y; if (vertices[i].pt.y > ye) ye = vertices[i].pt.y; } @@ -1135,68 +1146,3 @@ static bool _rasterTexmapPolygon(SwSurface* surface, const SwImage* image, const #endif return _apply(surface, aaSpans); } - - -/* - Provide any number of triangles to draw a mesh using the supplied image. - Indexes are not used, so each triangle (Polygon) vertex has to be defined, even if they copy the previous one. - Example: - - 0 -- 1 0 -- 1 0 - | / | --> | / / | - | / | | / / | - 2 -- 3 2 1 -- 2 - - Should provide two Polygons, one for each triangle. - // TODO: region? -*/ -static bool _rasterTexmapPolygonMesh(SwSurface* surface, const SwImage* image, const RenderMesh* mesh, const Matrix* transform, const SwBBox* region, uint8_t opacity) -{ - if (surface->channelSize == sizeof(uint8_t)) { - TVGERR("SW_ENGINE", "Not supported grayscale Textmap polygon mesh!"); - return false; - } - - //Exceptions: No dedicated drawing area? - if ((!image->rle && !region) || (image->rle && image->rle->size == 0)) return false; - - // Step polygons once to transform - auto transformedTris = (Polygon*)malloc(sizeof(Polygon) * mesh->triangleCnt); - float ys = FLT_MAX, ye = -1.0f; - for (uint32_t i = 0; i < mesh->triangleCnt; i++) { - transformedTris[i] = mesh->triangles[i]; - transformedTris[i].vertex[0].pt *= *transform; - transformedTris[i].vertex[1].pt *= *transform; - transformedTris[i].vertex[2].pt *= *transform; - - if (transformedTris[i].vertex[0].pt.y < ys) ys = transformedTris[i].vertex[0].pt.y; - else if (transformedTris[i].vertex[0].pt.y > ye) ye = transformedTris[i].vertex[0].pt.y; - if (transformedTris[i].vertex[1].pt.y < ys) ys = transformedTris[i].vertex[1].pt.y; - else if (transformedTris[i].vertex[1].pt.y > ye) ye = transformedTris[i].vertex[1].pt.y; - if (transformedTris[i].vertex[2].pt.y < ys) ys = transformedTris[i].vertex[2].pt.y; - else if (transformedTris[i].vertex[2].pt.y > ye) ye = transformedTris[i].vertex[2].pt.y; - - // Convert normalized UV coordinates to image coordinates - transformedTris[i].vertex[0].uv.x *= (float)image->w; - transformedTris[i].vertex[0].uv.y *= (float)image->h; - transformedTris[i].vertex[1].uv.x *= (float)image->w; - transformedTris[i].vertex[1].uv.y *= (float)image->h; - transformedTris[i].vertex[2].uv.x *= (float)image->w; - transformedTris[i].vertex[2].uv.y *= (float)image->h; - } - - // Get AA spans and step polygons again to draw - if (auto aaSpans = _AASpans(ys, ye, image, region)) { - for (uint32_t i = 0; i < mesh->triangleCnt; i++) { - _rasterPolygonImage(surface, image, region, transformedTris[i], aaSpans, opacity); - } -#if 0 - if (_compositing(surface) && _masking(surface) && !_direct(surface->compositor->method)) { - _compositeMaskImage(surface, &surface->compositor->image, surface->compositor->bbox); - } -#endif - _apply(surface, aaSpans); - } - free(transformedTris); - return true; -} diff --git a/thirdparty/thorvg/src/renderer/sw_engine/tvgSwRenderer.cpp b/thirdparty/thorvg/src/renderer/sw_engine/tvgSwRenderer.cpp index 350f333405..6470089c7c 100644 --- a/thirdparty/thorvg/src/renderer/sw_engine/tvgSwRenderer.cpp +++ b/thirdparty/thorvg/src/renderer/sw_engine/tvgSwRenderer.cpp @@ -39,7 +39,7 @@ struct SwTask : Task SwSurface* surface = nullptr; SwMpool* mpool = nullptr; SwBBox bbox = {{0, 0}, {0, 0}}; //Whole Rendering Region - Matrix* transform = nullptr; + Matrix transform; Array<RenderData> clips; RenderUpdateFlag flags = RenderUpdateFlag::None; uint8_t opacity; @@ -68,10 +68,7 @@ struct SwTask : Task virtual bool clip(SwRleData* target) = 0; virtual SwRleData* rle() = 0; - virtual ~SwTask() - { - free(transform); - } + virtual ~SwTask() {} }; @@ -100,11 +97,9 @@ struct SwShapeTask : SwTask if (!rshape->stroke->fill && (MULTIPLY(rshape->stroke->color[3], opacity) == 0)) return 0.0f; if (mathZero(rshape->stroke->trim.begin - rshape->stroke->trim.end)) return 0.0f; - if (transform) return (width * sqrt(transform->e11 * transform->e11 + transform->e12 * transform->e12)); - else return width; + return (width * sqrt(transform.e11 * transform.e11 + transform.e12 * transform.e12)); } - bool clip(SwRleData* target) override { if (shape.fastTrack) rleClipRect(target, &bbox); @@ -203,64 +198,10 @@ struct SwShapeTask : SwTask }; -struct SwSceneTask : SwTask -{ - Array<RenderData> scene; //list of paints render data (SwTask) - SwRleData* sceneRle = nullptr; - - bool clip(SwRleData* target) override - { - //Only one shape - if (scene.count == 1) { - return static_cast<SwTask*>(*scene.data)->clip(target); - } - - //More than one shapes - if (sceneRle) rleClipPath(target, sceneRle); - else TVGLOG("SW_ENGINE", "No clippers in a scene?"); - - return true; - } - - SwRleData* rle() override - { - return sceneRle; - } - - void run(unsigned tid) override - { - //TODO: Skip the run if the scene hans't changed. - if (!sceneRle) sceneRle = static_cast<SwRleData*>(calloc(1, sizeof(SwRleData))); - else rleReset(sceneRle); - - //Merge shapes if it has more than one shapes - if (scene.count > 1) { - //Merge first two clippers - auto clipper1 = static_cast<SwTask*>(*scene.data); - auto clipper2 = static_cast<SwTask*>(*(scene.data + 1)); - - rleMerge(sceneRle, clipper1->rle(), clipper2->rle()); - - //Unify the remained clippers - for (auto rd = scene.begin() + 2; rd < scene.end(); ++rd) { - auto clipper = static_cast<SwTask*>(*rd); - rleMerge(sceneRle, sceneRle, clipper->rle()); - } - } - } - - void dispose() override - { - rleFree(sceneRle); - } -}; - - struct SwImageTask : SwTask { SwImage image; Surface* source; //Image source - const RenderMesh* mesh = nullptr; //Should be valid ptr in action bool clip(SwRleData* target) override { @@ -293,10 +234,9 @@ struct SwImageTask : SwTask imageReset(&image); if (!image.data || image.w == 0 || image.h == 0) goto end; - if (!imagePrepare(&image, mesh, transform, clipRegion, bbox, mpool, tid)) goto end; + if (!imagePrepare(&image, transform, clipRegion, bbox, mpool, tid)) goto end; - // TODO: How do we clip the triangle mesh? Only clip non-meshed images for now - if (mesh->triangleCnt == 0 && clips.count > 0) { + if (clips.count > 0) { if (!imageGenRle(&image, bbox, false)) goto end; if (image.rle) { //Clear current task memorypool here if the clippers would use the same memory pool @@ -336,7 +276,7 @@ static void _renderFill(SwShapeTask* task, SwSurface* surface, uint8_t opacity) { uint8_t r, g, b, a; if (auto fill = task->rshape->fill) { - rasterGradientShape(surface, &task->shape, fill->identifier()); + rasterGradientShape(surface, &task->shape, fill, opacity); } else { task->rshape->fillColor(&r, &g, &b, &a); a = MULTIPLY(opacity, a); @@ -348,7 +288,7 @@ static void _renderStroke(SwShapeTask* task, SwSurface* surface, uint8_t opacity { uint8_t r, g, b, a; if (auto strokeFill = task->rshape->strokeFill()) { - rasterGradientStroke(surface, &task->shape, strokeFill->identifier()); + rasterGradientStroke(surface, &task->shape, strokeFill, opacity); } else { if (task->rshape->strokeColor(&r, &g, &b, &a)) { a = MULTIPLY(opacity, a); @@ -480,7 +420,7 @@ bool SwRenderer::renderImage(RenderData data) if (task->opacity == 0) return true; - return rasterImage(surface, &task->image, task->mesh, task->transform, task->bbox, task->opacity); + return rasterImage(surface, &task->image, task->transform, task->bbox, task->opacity); } @@ -506,7 +446,7 @@ bool SwRenderer::renderShape(RenderData data) } -bool SwRenderer::blend(BlendMethod method) +bool SwRenderer::blend(BlendMethod method, bool direct) { if (surface->blendMethod == method) return true; surface->blendMethod = method; @@ -519,7 +459,7 @@ bool SwRenderer::blend(BlendMethod method) surface->blender = opBlendScreen; break; case BlendMethod::Multiply: - surface->blender = opBlendMultiply; + surface->blender = direct ? opBlendDirectMultiply : opBlendMultiply; break; case BlendMethod::Overlay: surface->blender = opBlendOverlay; @@ -688,7 +628,8 @@ bool SwRenderer::endComposite(Compositor* cmp) //Default is alpha blending if (p->method == CompositeMethod::None) { - return rasterImage(surface, &p->image, nullptr, nullptr, p->bbox, p->opacity); + Matrix m = {1, 0, 0, 0, 1, 0, 0, 0, 1}; + return rasterImage(surface, &p->image, m, p->bbox, p->opacity); } return true; @@ -714,7 +655,7 @@ void SwRenderer::dispose(RenderData data) } -void* SwRenderer::prepareCommon(SwTask* task, const RenderTransform* transform, const Array<RenderData>& clips, uint8_t opacity, RenderUpdateFlag flags) +void* SwRenderer::prepareCommon(SwTask* task, const Matrix& transform, const Array<RenderData>& clips, uint8_t opacity, RenderUpdateFlag flags) { if (!surface) return task; if (flags == RenderUpdateFlag::None) return task; @@ -727,20 +668,11 @@ void* SwRenderer::prepareCommon(SwTask* task, const RenderTransform* transform, } task->clips = clips; - - if (transform) { - if (!task->transform) task->transform = static_cast<Matrix*>(malloc(sizeof(Matrix))); - *task->transform = transform->m; - } else { - if (task->transform) free(task->transform); - task->transform = nullptr; - } - + task->transform = transform; + //zero size? - if (task->transform) { - if (task->transform->e11 == 0.0f && task->transform->e12 == 0.0f) return task; //zero width - if (task->transform->e21 == 0.0f && task->transform->e22 == 0.0f) return task; //zero height - } + if (task->transform.e11 == 0.0f && task->transform.e12 == 0.0f) return task; //zero width + if (task->transform.e21 == 0.0f && task->transform.e22 == 0.0f) return task; //zero height task->opacity = opacity; task->surface = surface; @@ -762,7 +694,7 @@ void* SwRenderer::prepareCommon(SwTask* task, const RenderTransform* transform, } -RenderData SwRenderer::prepare(Surface* surface, const RenderMesh* mesh, RenderData data, const RenderTransform* transform, Array<RenderData>& clips, uint8_t opacity, RenderUpdateFlag flags) +RenderData SwRenderer::prepare(Surface* surface, RenderData data, const Matrix& transform, Array<RenderData>& clips, uint8_t opacity, RenderUpdateFlag flags) { //prepare task auto task = static_cast<SwImageTask*>(data); @@ -770,33 +702,12 @@ RenderData SwRenderer::prepare(Surface* surface, const RenderMesh* mesh, RenderD else task->done(); task->source = surface; - task->mesh = mesh; - - return prepareCommon(task, transform, clips, opacity, flags); -} - - -RenderData SwRenderer::prepare(const Array<RenderData>& scene, RenderData data, const RenderTransform* transform, Array<RenderData>& clips, uint8_t opacity, RenderUpdateFlag flags) -{ - //prepare task - auto task = static_cast<SwSceneTask*>(data); - if (!task) task = new SwSceneTask; - else task->done(); - - task->scene = scene; - - //TODO: Failed threading them. It would be better if it's possible. - //See: https://github.com/thorvg/thorvg/issues/1409 - //Guarantee composition targets get ready. - for (auto task = scene.begin(); task < scene.end(); ++task) { - static_cast<SwTask*>(*task)->done(); - } return prepareCommon(task, transform, clips, opacity, flags); } -RenderData SwRenderer::prepare(const RenderShape& rshape, RenderData data, const RenderTransform* transform, Array<RenderData>& clips, uint8_t opacity, RenderUpdateFlag flags, bool clipper) +RenderData SwRenderer::prepare(const RenderShape& rshape, RenderData data, const Matrix& transform, Array<RenderData>& clips, uint8_t opacity, RenderUpdateFlag flags, bool clipper) { //prepare task auto task = static_cast<SwShapeTask*>(data); diff --git a/thirdparty/thorvg/src/renderer/sw_engine/tvgSwRenderer.h b/thirdparty/thorvg/src/renderer/sw_engine/tvgSwRenderer.h index 57be558988..7aa6d89aaa 100644 --- a/thirdparty/thorvg/src/renderer/sw_engine/tvgSwRenderer.h +++ b/thirdparty/thorvg/src/renderer/sw_engine/tvgSwRenderer.h @@ -36,9 +36,8 @@ namespace tvg class SwRenderer : public RenderMethod { public: - RenderData prepare(const RenderShape& rshape, RenderData data, const RenderTransform* transform, Array<RenderData>& clips, uint8_t opacity, RenderUpdateFlag flags, bool clipper) override; - RenderData prepare(const Array<RenderData>& scene, RenderData data, const RenderTransform* transform, Array<RenderData>& clips, uint8_t opacity, RenderUpdateFlag flags) override; - RenderData prepare(Surface* surface, const RenderMesh* mesh, RenderData data, const RenderTransform* transform, Array<RenderData>& clips, uint8_t opacity, RenderUpdateFlag flags) override; + RenderData prepare(const RenderShape& rshape, RenderData data, const Matrix& transform, Array<RenderData>& clips, uint8_t opacity, RenderUpdateFlag flags, bool clipper) override; + RenderData prepare(Surface* surface, RenderData data, const Matrix& transform, Array<RenderData>& clips, uint8_t opacity, RenderUpdateFlag flags) override; bool preRender() override; bool renderShape(RenderData data) override; bool renderImage(RenderData data) override; @@ -47,7 +46,7 @@ public: RenderRegion region(RenderData data) override; RenderRegion viewport() override; bool viewport(const RenderRegion& vp) override; - bool blend(BlendMethod method) override; + bool blend(BlendMethod method, bool direct) override; ColorSpace colorSpace() override; const Surface* mainSurface() override; @@ -77,7 +76,7 @@ private: SwRenderer(); ~SwRenderer(); - RenderData prepareCommon(SwTask* task, const RenderTransform* transform, const Array<RenderData>& clips, uint8_t opacity, RenderUpdateFlag flags); + RenderData prepareCommon(SwTask* task, const Matrix& transform, const Array<RenderData>& clips, uint8_t opacity, RenderUpdateFlag flags); }; } diff --git a/thirdparty/thorvg/src/renderer/sw_engine/tvgSwRle.cpp b/thirdparty/thorvg/src/renderer/sw_engine/tvgSwRle.cpp index 25c6cd90b9..42b08de6a5 100644 --- a/thirdparty/thorvg/src/renderer/sw_engine/tvgSwRle.cpp +++ b/thirdparty/thorvg/src/renderer/sw_engine/tvgSwRle.cpp @@ -358,7 +358,7 @@ static void _horizLine(RleWorker& rw, SwCoord x, SwCoord y, SwCoord area, SwCoor rle->spans = static_cast<SwSpan*>(realloc(rle->spans, rle->alloc * sizeof(SwSpan))); } } - + //Clip x range SwCoord xOver = 0; if (x + acount >= rw.cellMax.x) xOver -= (x + acount - rw.cellMax.x); @@ -822,46 +822,6 @@ static SwSpan* _intersectSpansRect(const SwBBox *bbox, const SwRleData *targetRl } -static SwSpan* _mergeSpansRegion(const SwRleData *clip1, const SwRleData *clip2, SwSpan *outSpans) -{ - auto out = outSpans; - auto spans1 = clip1->spans; - auto end1 = clip1->spans + clip1->size; - auto spans2 = clip2->spans; - auto end2 = clip2->spans + clip2->size; - - //list two spans up in y order - //TODO: Remove duplicated regions? - while (spans1 < end1 && spans2 < end2) { - while (spans1 < end1 && spans1->y <= spans2->y) { - *out = *spans1; - ++spans1; - ++out; - } - if (spans1 >= end1) break; - while (spans2 < end2 && spans2->y <= spans1->y) { - *out = *spans2; - ++spans2; - ++out; - } - } - - //Leftovers - while (spans1 < end1) { - *out = *spans1; - ++spans1; - ++out; - } - while (spans2 < end2) { - *out = *spans2; - ++spans2; - ++out; - } - - return out; -} - - void _replaceClipSpan(SwRleData *rle, SwSpan* clippedSpans, uint32_t size) { free(rle->spans); @@ -1030,45 +990,6 @@ void rleFree(SwRleData* rle) } -void rleMerge(SwRleData* rle, SwRleData* clip1, SwRleData* clip2) -{ - if (!rle || (!clip1 && !clip2)) return; - if (clip1 && clip1->size == 0 && clip2 && clip2->size == 0) return; - - TVGLOG("SW_ENGINE", "Unifying Rle!"); - - //clip1 is empty, just copy clip2 - if (!clip1 || clip1->size == 0) { - if (clip2) { - auto spans = static_cast<SwSpan*>(malloc(sizeof(SwSpan) * (clip2->size))); - memcpy(spans, clip2->spans, clip2->size); - _replaceClipSpan(rle, spans, clip2->size); - } else { - _replaceClipSpan(rle, nullptr, 0); - } - return; - } - - //clip2 is empty, just copy clip1 - if (!clip2 || clip2->size == 0) { - if (clip1) { - auto spans = static_cast<SwSpan*>(malloc(sizeof(SwSpan) * (clip1->size))); - memcpy(spans, clip1->spans, clip1->size); - _replaceClipSpan(rle, spans, clip1->size); - } else { - _replaceClipSpan(rle, nullptr, 0); - } - return; - } - - auto spanCnt = clip1->size + clip2->size; - auto spans = static_cast<SwSpan*>(malloc(sizeof(SwSpan) * spanCnt)); - auto spansEnd = _mergeSpansRegion(clip1, clip2, spans); - - _replaceClipSpan(rle, spans, spansEnd - spans); -} - - void rleClipPath(SwRleData *rle, const SwRleData *clip) { if (rle->size == 0 || clip->size == 0) return; diff --git a/thirdparty/thorvg/src/renderer/sw_engine/tvgSwShape.cpp b/thirdparty/thorvg/src/renderer/sw_engine/tvgSwShape.cpp index 4f069ece97..96c3bc28b9 100644 --- a/thirdparty/thorvg/src/renderer/sw_engine/tvgSwShape.cpp +++ b/thirdparty/thorvg/src/renderer/sw_engine/tvgSwShape.cpp @@ -49,7 +49,7 @@ static bool _outlineEnd(SwOutline& outline) } -static bool _outlineMoveTo(SwOutline& outline, const Point* to, const Matrix* transform, bool closed = false) +static bool _outlineMoveTo(SwOutline& outline, const Point* to, const Matrix& transform, bool closed = false) { //make it a contour, if the last contour is not closed yet. if (!closed) _outlineEnd(outline); @@ -60,14 +60,14 @@ static bool _outlineMoveTo(SwOutline& outline, const Point* to, const Matrix* tr } -static void _outlineLineTo(SwOutline& outline, const Point* to, const Matrix* transform) +static void _outlineLineTo(SwOutline& outline, const Point* to, const Matrix& transform) { outline.pts.push(mathTransform(to, transform)); outline.types.push(SW_CURVE_TYPE_POINT); } -static void _outlineCubicTo(SwOutline& outline, const Point* ctrl1, const Point* ctrl2, const Point* to, const Matrix* transform) +static void _outlineCubicTo(SwOutline& outline, const Point* ctrl1, const Point* ctrl2, const Point* to, const Matrix& transform) { outline.pts.push(mathTransform(ctrl1, transform)); outline.types.push(SW_CURVE_TYPE_CUBIC); @@ -99,7 +99,7 @@ static bool _outlineClose(SwOutline& outline) } -static void _dashLineTo(SwDashStroke& dash, const Point* to, const Matrix* transform) +static void _dashLineTo(SwDashStroke& dash, const Point* to, const Matrix& transform) { Line cur = {dash.ptCur, *to}; auto len = lineLength(cur.pt1, cur.pt2); @@ -160,7 +160,7 @@ static void _dashLineTo(SwDashStroke& dash, const Point* to, const Matrix* trans } -static void _dashCubicTo(SwDashStroke& dash, const Point* ctrl1, const Point* ctrl2, const Point* to, const Matrix* transform) +static void _dashCubicTo(SwDashStroke& dash, const Point* ctrl1, const Point* ctrl2, const Point* to, const Matrix& transform) { Bezier cur = {dash.ptCur, *ctrl1, *ctrl2, *to}; auto len = bezLength(cur); @@ -221,7 +221,7 @@ static void _dashCubicTo(SwDashStroke& dash, const Point* ctrl1, const Point* ct } -static void _dashClose(SwDashStroke& dash, const Matrix* transform) +static void _dashClose(SwDashStroke& dash, const Matrix& transform) { _dashLineTo(dash, &dash.ptStart, transform); } @@ -245,10 +245,10 @@ static void _dashMoveTo(SwDashStroke& dash, uint32_t offIdx, float offset, const } -static void _trimPattern(SwDashStroke* dash, const RenderShape* rshape, float length) +static void _trimPattern(SwDashStroke* dash, const RenderShape* rshape, float length, float trimBegin, float trimEnd) { - auto begin = length * rshape->stroke->trim.begin; - auto end = length * rshape->stroke->trim.end; + auto begin = length * trimBegin; + auto end = length * trimEnd; //default if (end > begin) { @@ -324,7 +324,7 @@ static float _outlineLength(const RenderShape* rshape, uint32_t shiftPts, uint32 } -static SwOutline* _genDashOutline(const RenderShape* rshape, const Matrix* transform, bool trimmed, SwMpool* mpool, unsigned tid) +static SwOutline* _genDashOutline(const RenderShape* rshape, const Matrix& transform, bool trimmed, SwMpool* mpool, unsigned tid) { const PathCommand* cmds = rshape->path.cmds.data; auto cmdCnt = rshape->path.cmds.count; @@ -341,6 +341,8 @@ static SwOutline* _genDashOutline(const RenderShape* rshape, const Matrix* trans auto offset = 0.0f; dash.cnt = rshape->strokeDash((const float**)&dash.pattern, &offset); auto simultaneous = rshape->stroke->trim.simultaneous; + float trimBegin = 0.0f, trimEnd = 1.0f; + if (trimmed) rshape->stroke->strokeTrim(trimBegin, trimEnd); if (dash.cnt == 0) { if (trimmed) dash.pattern = (float*)malloc(sizeof(float) * 4); @@ -372,7 +374,7 @@ static SwOutline* _genDashOutline(const RenderShape* rshape, const Matrix* trans //must begin with moveTo if (cmds[0] == PathCommand::MoveTo) { - if (trimmed) _trimPattern(&dash, rshape, _outlineLength(rshape, 0, 0, simultaneous)); + if (trimmed) _trimPattern(&dash, rshape, _outlineLength(rshape, 0, 0, simultaneous), trimBegin, trimEnd); _dashMoveTo(dash, offIdx, offset, pts); cmds++; pts++; @@ -387,7 +389,7 @@ static SwOutline* _genDashOutline(const RenderShape* rshape, const Matrix* trans case PathCommand::MoveTo: { if (trimmed) { if (simultaneous) { - _trimPattern(&dash, rshape, _outlineLength(rshape, pts - startPts, cmds - startCmds, true)); + _trimPattern(&dash, rshape, _outlineLength(rshape, pts - startPts, cmds - startCmds, true), trimBegin, trimEnd); _dashMoveTo(dash, offIdx, offset, pts); } else _dashMoveTo(dash, pts); } else _dashMoveTo(dash, offIdx, offset, pts); @@ -436,7 +438,7 @@ static bool _axisAlignedRect(const SwOutline* outline) } -static bool _genOutline(SwShape* shape, const RenderShape* rshape, const Matrix* transform, SwMpool* mpool, unsigned tid, bool hasComposite) +static bool _genOutline(SwShape* shape, const RenderShape* rshape, const Matrix& transform, SwMpool* mpool, unsigned tid, bool hasComposite) { const PathCommand* cmds = rshape->path.cmds.data; auto cmdCnt = rshape->path.cmds.count; @@ -492,7 +494,7 @@ static bool _genOutline(SwShape* shape, const RenderShape* rshape, const Matrix* /* External Class Implementation */ /************************************************************************/ -bool shapePrepare(SwShape* shape, const RenderShape* rshape, const Matrix* transform, const SwBBox& clipRegion, SwBBox& renderRegion, SwMpool* mpool, unsigned tid, bool hasComposite) +bool shapePrepare(SwShape* shape, const RenderShape* rshape, const Matrix& transform, const SwBBox& clipRegion, SwBBox& renderRegion, SwMpool* mpool, unsigned tid, bool hasComposite) { if (!_genOutline(shape, rshape, transform, mpool, tid, hasComposite)) return false; if (!mathUpdateOutlineBBox(shape->outline, clipRegion, renderRegion, shape->fastTrack)) return false; @@ -575,7 +577,7 @@ void shapeDelStroke(SwShape* shape) } -void shapeResetStroke(SwShape* shape, const RenderShape* rshape, const Matrix* transform) +void shapeResetStroke(SwShape* shape, const RenderShape* rshape, const Matrix& transform) { if (!shape->stroke) shape->stroke = static_cast<SwStroke*>(calloc(1, sizeof(SwStroke))); auto stroke = shape->stroke; @@ -586,7 +588,7 @@ void shapeResetStroke(SwShape* shape, const RenderShape* rshape, const Matrix* t } -bool shapeGenStrokeRle(SwShape* shape, const RenderShape* rshape, const Matrix* transform, const SwBBox& clipRegion, SwBBox& renderRegion, SwMpool* mpool, unsigned tid) +bool shapeGenStrokeRle(SwShape* shape, const RenderShape* rshape, const Matrix& transform, const SwBBox& clipRegion, SwBBox& renderRegion, SwMpool* mpool, unsigned tid) { SwOutline* shapeOutline = nullptr; SwOutline* strokeOutline = nullptr; @@ -629,13 +631,13 @@ clear: } -bool shapeGenFillColors(SwShape* shape, const Fill* fill, const Matrix* transform, SwSurface* surface, uint8_t opacity, bool ctable) +bool shapeGenFillColors(SwShape* shape, const Fill* fill, const Matrix& transform, SwSurface* surface, uint8_t opacity, bool ctable) { return fillGenColorTable(shape->fill, fill, transform, surface, opacity, ctable); } -bool shapeGenStrokeFillColors(SwShape* shape, const Fill* fill, const Matrix* transform, SwSurface* surface, uint8_t opacity, bool ctable) +bool shapeGenStrokeFillColors(SwShape* shape, const Fill* fill, const Matrix& transform, SwSurface* surface, uint8_t opacity, bool ctable) { return fillGenColorTable(shape->stroke->fill, fill, transform, surface, opacity, ctable); } diff --git a/thirdparty/thorvg/src/renderer/sw_engine/tvgSwStroke.cpp b/thirdparty/thorvg/src/renderer/sw_engine/tvgSwStroke.cpp index 18f5f3eca8..e0e74ce53c 100644 --- a/thirdparty/thorvg/src/renderer/sw_engine/tvgSwStroke.cpp +++ b/thirdparty/thorvg/src/renderer/sw_engine/tvgSwStroke.cpp @@ -441,11 +441,17 @@ static void _cubicTo(SwStroke& stroke, const SwPoint& ctrl1, const SwPoint& ctrl //initialize with current direction angleIn = angleOut = angleMid = stroke.angleIn; - if (arc < limit && !mathSmallCubic(arc, angleIn, angleMid, angleOut)) { - if (stroke.firstPt) stroke.angleIn = angleIn; - mathSplitCubic(arc); - arc += 3; - continue; + if (arc < limit) { + if (mathSmallCubic(arc)) { + arc -= 3; + continue; + } + if (!mathFlatCubic(arc, angleIn, angleMid, angleOut)) { + if (stroke.firstPt) stroke.angleIn = angleIn; + mathSplitCubic(arc); + arc += 3; + continue; + } } if (firstArc) { @@ -805,15 +811,10 @@ void strokeFree(SwStroke* stroke) } -void strokeReset(SwStroke* stroke, const RenderShape* rshape, const Matrix* transform) +void strokeReset(SwStroke* stroke, const RenderShape* rshape, const Matrix& transform) { - if (transform) { - stroke->sx = sqrtf(powf(transform->e11, 2.0f) + powf(transform->e21, 2.0f)); - stroke->sy = sqrtf(powf(transform->e12, 2.0f) + powf(transform->e22, 2.0f)); - } else { - stroke->sx = stroke->sy = 1.0f; - } - + stroke->sx = sqrtf(powf(transform.e11, 2.0f) + powf(transform.e21, 2.0f)); + stroke->sy = sqrtf(powf(transform.e12, 2.0f) + powf(transform.e22, 2.0f)); stroke->width = HALF_STROKE(rshape->strokeWidth()); stroke->cap = rshape->strokeCap(); stroke->miterlimit = static_cast<SwFixed>(rshape->strokeMiterlimit() * 65536.0f); diff --git a/thirdparty/thorvg/src/renderer/tvgAccessor.cpp b/thirdparty/thorvg/src/renderer/tvgAccessor.cpp index 903437f29d..a144726804 100644 --- a/thirdparty/thorvg/src/renderer/tvgAccessor.cpp +++ b/thirdparty/thorvg/src/renderer/tvgAccessor.cpp @@ -21,20 +21,21 @@ */ #include "tvgIteratorAccessor.h" +#include "tvgCompressor.h" /************************************************************************/ /* Internal Class Implementation */ /************************************************************************/ -static bool accessChildren(Iterator* it, function<bool(const Paint* paint)> func) +static bool accessChildren(Iterator* it, function<bool(const Paint* paint, void* data)> func, void* data) { while (auto child = it->next()) { //Access the child - if (!func(child)) return false; + if (!func(child, data)) return false; //Access the children of the child if (auto it2 = IteratorAccessor::iterator(child)) { - if (!accessChildren(it2, func)) { + if (!accessChildren(it2, func, data)) { delete(it2); return false; } @@ -44,26 +45,46 @@ static bool accessChildren(Iterator* it, function<bool(const Paint* paint)> func return true; } + /************************************************************************/ /* External Class Implementation */ /************************************************************************/ -unique_ptr<Picture> Accessor::set(unique_ptr<Picture> picture, function<bool(const Paint* paint)> func) noexcept +TVG_DEPRECATED unique_ptr<Picture> Accessor::set(unique_ptr<Picture> picture, function<bool(const Paint* paint)> func) noexcept +{ + auto backward = [](const tvg::Paint* paint, void* data) -> bool + { + auto func = reinterpret_cast<function<bool(const Paint* paint)>*>(data); + if (!(*func)(paint)) return false; + return true; + }; + + set(picture.get(), backward, reinterpret_cast<void*>(&func)); + return picture; +} + + +Result Accessor::set(const Picture* picture, function<bool(const Paint* paint, void* data)> func, void* data) noexcept { - auto p = picture.get(); - if (!p || !func) return picture; + if (!picture || !func) return Result::InvalidArguments; //Use the Preorder Tree-Search //Root - if (!func(p)) return picture; + if (!func(picture, data)) return Result::Success; //Children - if (auto it = IteratorAccessor::iterator(p)) { - accessChildren(it, func); + if (auto it = IteratorAccessor::iterator(picture)) { + accessChildren(it, func, data); delete(it); } - return picture; + return Result::Success; +} + + +uint32_t Accessor::id(const char* name) noexcept +{ + return djb2Encode(name); } diff --git a/thirdparty/thorvg/src/renderer/tvgAnimation.cpp b/thirdparty/thorvg/src/renderer/tvgAnimation.cpp index be6c2dc7de..6c4711e8c1 100644 --- a/thirdparty/thorvg/src/renderer/tvgAnimation.cpp +++ b/thirdparty/thorvg/src/renderer/tvgAnimation.cpp @@ -95,7 +95,7 @@ float Animation::duration() const noexcept Result Animation::segment(float begin, float end) noexcept { - if (begin < 0.0 || end > 1.0 || begin >= end) return Result::InvalidArguments; + if (begin < 0.0 || end > 1.0 || begin > end) return Result::InvalidArguments; auto loader = pImpl->picture->pImpl->loader; if (!loader) return Result::InsufficientCondition; diff --git a/thirdparty/thorvg/src/renderer/tvgCanvas.h b/thirdparty/thorvg/src/renderer/tvgCanvas.h index 81fd1b7d6f..199e823034 100644 --- a/thirdparty/thorvg/src/renderer/tvgCanvas.h +++ b/thirdparty/thorvg/src/renderer/tvgCanvas.h @@ -93,11 +93,13 @@ struct Canvas::Impl auto flag = RenderUpdateFlag::None; if (status == Status::Damanged || force) flag = RenderUpdateFlag::All; + auto m = Matrix{1, 0, 0, 0, 1, 0, 0, 0, 1}; + if (paint) { - paint->pImpl->update(renderer, nullptr, clips, 255, flag); + paint->pImpl->update(renderer, m, clips, 255, flag); } else { for (auto paint : paints) { - paint->pImpl->update(renderer, nullptr, clips, 255, flag); + paint->pImpl->update(renderer, m, clips, 255, flag); } } status = Status::Updating; diff --git a/thirdparty/thorvg/src/renderer/tvgInitializer.cpp b/thirdparty/thorvg/src/renderer/tvgInitializer.cpp index 76d89b40ed..c57b20779c 100644 --- a/thirdparty/thorvg/src/renderer/tvgInitializer.cpp +++ b/thirdparty/thorvg/src/renderer/tvgInitializer.cpp @@ -54,36 +54,30 @@ static constexpr bool operator &(CanvasEngine a, CanvasEngine b) return int(a) & int(b); } -static bool _buildVersionInfo() +static bool _buildVersionInfo(uint32_t* major, uint32_t* minor, uint32_t* micro) { - auto SRC = THORVG_VERSION_STRING; //ex) 0.3.99 - auto p = SRC; + auto VER = THORVG_VERSION_STRING; + auto p = VER; const char* x; - char major[3]; - x = strchr(p, '.'); - if (!x) return false; - memcpy(major, p, x - p); - major[x - p] = '\0'; + if (!(x = strchr(p, '.'))) return false; + uint32_t majorVal = atoi(p); p = x + 1; - char minor[3]; - x = strchr(p, '.'); - if (!x) return false; - memcpy(minor, p, x - p); - minor[x - p] = '\0'; + if (!(x = strchr(p, '.'))) return false; + uint32_t minorVal = atoi(p); p = x + 1; - char micro[3]; - x = SRC + strlen(THORVG_VERSION_STRING); - memcpy(micro, p, x - p); - micro[x - p] = '\0'; + uint32_t microVal = atoi(p); char sum[7]; - snprintf(sum, sizeof(sum), "%s%s%s", major, minor, micro); - + snprintf(sum, sizeof(sum), "%d%02d%02d", majorVal, minorVal, microVal); _version = atoi(sum); + if (major) *major = majorVal; + if (minor) *minor = minorVal; + if (micro) *micro = microVal; + return true; } @@ -122,7 +116,7 @@ Result Initializer::init(CanvasEngine engine, uint32_t threads) noexcept if (_initCnt++ > 0) return Result::Success; - if (!_buildVersionInfo()) return Result::Unknown; + if (!_buildVersionInfo(nullptr, nullptr, nullptr)) return Result::Unknown; if (!LoaderMgr::init()) return Result::Unknown; @@ -172,8 +166,14 @@ Result Initializer::term(CanvasEngine engine) noexcept } +const char* Initializer::version(uint32_t* major, uint32_t* minor, uint32_t* micro) noexcept +{ + if ((!major && ! minor && !micro) || _buildVersionInfo(major, minor, micro)) return THORVG_VERSION_STRING; + return nullptr; +} + + uint16_t THORVG_VERSION_NUMBER() { return _version; } - diff --git a/thirdparty/thorvg/src/renderer/tvgLoadModule.h b/thirdparty/thorvg/src/renderer/tvgLoadModule.h index c750683771..1b81d81a4f 100644 --- a/thirdparty/thorvg/src/renderer/tvgLoadModule.h +++ b/thirdparty/thorvg/src/renderer/tvgLoadModule.h @@ -101,7 +101,8 @@ struct FontLoader : LoadModule FontLoader(FileType type) : LoadModule(type) {} - virtual bool request(Shape* shape, char* text, bool italic = false) = 0; + virtual bool request(Shape* shape, char* text) = 0; + virtual bool transform(Paint* paint, float fontSize, bool italic) = 0; }; #endif //_TVG_LOAD_MODULE_H_ diff --git a/thirdparty/thorvg/src/renderer/tvgPaint.cpp b/thirdparty/thorvg/src/renderer/tvgPaint.cpp index 0ce6540f20..37813b19ef 100644 --- a/thirdparty/thorvg/src/renderer/tvgPaint.cpp +++ b/thirdparty/thorvg/src/renderer/tvgPaint.cpp @@ -41,7 +41,7 @@ } -static Result _clipRect(RenderMethod* renderer, const Point* pts, const RenderTransform* pTransform, RenderTransform* rTransform, RenderRegion& before) +static Result _clipRect(RenderMethod* renderer, const Point* pts, const Matrix& pm, const Matrix& rm, RenderRegion& before) { //sorting Point tmp[4]; @@ -50,8 +50,8 @@ static Result _clipRect(RenderMethod* renderer, const Point* pts, const RenderTr for (int i = 0; i < 4; ++i) { tmp[i] = pts[i]; - if (rTransform) tmp[i] *= rTransform->m; - if (pTransform) tmp[i] *= pTransform->m; + tmp[i] *= rm; + tmp[i] *= pm; if (tmp[i].x < min.x) min.x = tmp[i].x; if (tmp[i].x > max.x) max.x = tmp[i].x; if (tmp[i].y < min.y) min.y = tmp[i].y; @@ -73,7 +73,7 @@ static Result _clipRect(RenderMethod* renderer, const Point* pts, const RenderTr } -static Result _compFastTrack(RenderMethod* renderer, Paint* cmpTarget, const RenderTransform* pTransform, RenderTransform* rTransform, RenderRegion& before) +static Result _compFastTrack(RenderMethod* renderer, Paint* cmpTarget, const Matrix& pm, RenderRegion& before) { /* Access Shape class by Paint is bad... but it's ok still it's an internal usage. */ auto shape = static_cast<Shape*>(cmpTarget); @@ -84,18 +84,17 @@ static Result _compFastTrack(RenderMethod* renderer, Paint* cmpTarget, const Ren //nothing to clip if (ptsCnt == 0) return Result::InvalidArguments; - if (ptsCnt != 4) return Result::InsufficientCondition; - if (rTransform && (cmpTarget->pImpl->renderFlag & RenderUpdateFlag::Transform)) rTransform->update(); + auto& rm = P(cmpTarget)->transform(); //No rotation and no skewing, still can try out clipping the rect region. auto tryClip = false; - if (pTransform && (!mathRightAngle(&pTransform->m) || mathSkewed(&pTransform->m))) tryClip = true; - if (rTransform && (!mathRightAngle(&rTransform->m) || mathSkewed(&rTransform->m))) tryClip = true; + if ((!mathRightAngle(pm) || mathSkewed(pm))) tryClip = true; + if ((!mathRightAngle(rm) || mathSkewed(rm))) tryClip = true; - if (tryClip) return _clipRect(renderer, pts, pTransform, rTransform, before); + if (tryClip) return _clipRect(renderer, pts, pm, rm, before); //Perpendicular Rectangle? auto pt1 = pts + 0; @@ -110,16 +109,10 @@ static Result _compFastTrack(RenderMethod* renderer, Paint* cmpTarget, const Ren auto v1 = *pt1; auto v2 = *pt3; - - if (rTransform) { - v1 *= rTransform->m; - v2 *= rTransform->m; - } - - if (pTransform) { - v1 *= pTransform->m; - v2 *= pTransform->m; - } + v1 *= rm; + v2 *= rm; + v1 *= pm; + v2 *= pm; //sorting if (v1.x > v2.x) std::swap(v1.x, v2.x); @@ -158,17 +151,15 @@ Iterator* Paint::Impl::iterator() } -Paint* Paint::Impl::duplicate() +Paint* Paint::Impl::duplicate(Paint* ret) { - Paint* ret; - PAINT_METHOD(ret, duplicate()); + if (ret) ret->composite(nullptr, CompositeMethod::None); + + PAINT_METHOD(ret, duplicate(ret)); //duplicate Transform - if (rTransform) { - ret->pImpl->rTransform = new RenderTransform(); - *ret->pImpl->rTransform = *rTransform; - ret->pImpl->renderFlag |= RenderUpdateFlag::Transform; - } + ret->pImpl->tr = tr; + ret->pImpl->renderFlag |= RenderUpdateFlag::Transform; ret->pImpl->opacity = opacity; @@ -180,14 +171,9 @@ Paint* Paint::Impl::duplicate() bool Paint::Impl::rotate(float degree) { - if (rTransform) { - if (rTransform->overriding) return false; - if (mathEqual(degree, rTransform->degree)) return true; - } else { - if (mathZero(degree)) return true; - rTransform = new RenderTransform(); - } - rTransform->degree = degree; + if (tr.overriding) return false; + if (mathEqual(degree, tr.degree)) return true; + tr.degree = degree; renderFlag |= RenderUpdateFlag::Transform; return true; @@ -196,14 +182,9 @@ bool Paint::Impl::rotate(float degree) bool Paint::Impl::scale(float factor) { - if (rTransform) { - if (rTransform->overriding) return false; - if (mathEqual(factor, rTransform->scale)) return true; - } else { - if (mathEqual(factor, 1.0f)) return true; - rTransform = new RenderTransform(); - } - rTransform->scale = factor; + if (tr.overriding) return false; + if (mathEqual(factor, tr.scale)) return true; + tr.scale = factor; renderFlag |= RenderUpdateFlag::Transform; return true; @@ -212,15 +193,10 @@ bool Paint::Impl::scale(float factor) bool Paint::Impl::translate(float x, float y) { - if (rTransform) { - if (rTransform->overriding) return false; - if (mathEqual(x, rTransform->m.e13) && mathEqual(y, rTransform->m.e23)) return true; - } else { - if (mathZero(x) && mathZero(y)) return true; - rTransform = new RenderTransform(); - } - rTransform->m.e13 = x; - rTransform->m.e23 = y; + if (tr.overriding) return false; + if (mathEqual(x, tr.m.e13) && mathEqual(y, tr.m.e23)) return true; + tr.m.e13 = x; + tr.m.e23 = y; renderFlag |= RenderUpdateFlag::Transform; return true; @@ -229,6 +205,8 @@ bool Paint::Impl::translate(float x, float y) bool Paint::Impl::render(RenderMethod* renderer) { + if (opacity == 0) return true; + Compositor* cmp = nullptr; /* Note: only ClipPath is processed in update() step. @@ -247,8 +225,6 @@ bool Paint::Impl::render(RenderMethod* renderer) if (cmp) renderer->beginComposite(cmp, compData->method, compData->target->pImpl->opacity); - renderer->blend(blendMethod); - bool ret; PAINT_METHOD(ret, render(renderer)); @@ -258,7 +234,7 @@ bool Paint::Impl::render(RenderMethod* renderer) } -RenderData Paint::Impl::update(RenderMethod* renderer, const RenderTransform* pTransform, Array<RenderData>& clips, uint8_t opacity, RenderUpdateFlag pFlag, bool clipper) +RenderData Paint::Impl::update(RenderMethod* renderer, const Matrix& pm, Array<RenderData>& clips, uint8_t opacity, RenderUpdateFlag pFlag, bool clipper) { if (this->renderer != renderer) { if (this->renderer) TVGERR("RENDERER", "paint's renderer has been changed!"); @@ -266,7 +242,7 @@ RenderData Paint::Impl::update(RenderMethod* renderer, const RenderTransform* pT this->renderer = renderer; } - if (renderFlag & RenderUpdateFlag::Transform) rTransform->update(); + if (renderFlag & RenderUpdateFlag::Transform) tr.update(); /* 1. Composition Pre Processing */ RenderData trd = nullptr; //composite target render data @@ -277,7 +253,7 @@ RenderData Paint::Impl::update(RenderMethod* renderer, const RenderTransform* pT if (compData) { auto target = compData->target; auto method = compData->method; - target->pImpl->ctxFlag &= ~ContextFlag::FastTrack; //reset + P(target)->ctxFlag &= ~ContextFlag::FastTrack; //reset /* If the transformation has no rotational factors and the ClipPath/Alpha(InvAlpha)Masking involves a simple rectangle, we can optimize by using the viewport instead of the regular ClipPath/AlphaMasking sequence for improved performance. */ @@ -296,14 +272,14 @@ RenderData Paint::Impl::update(RenderMethod* renderer, const RenderTransform* pT } if (tryFastTrack) { viewport = renderer->viewport(); - if ((compFastTrack = _compFastTrack(renderer, target, pTransform, target->pImpl->rTransform, viewport)) == Result::Success) { - target->pImpl->ctxFlag |= ContextFlag::FastTrack; + if ((compFastTrack = _compFastTrack(renderer, target, pm, viewport)) == Result::Success) { + P(target)->ctxFlag |= ContextFlag::FastTrack; } } } if (compFastTrack == Result::InsufficientCondition) { childClipper = compData->method == CompositeMethod::ClipPath ? true : false; - trd = target->pImpl->update(renderer, pTransform, clips, 255, pFlag, childClipper); + trd = P(target)->update(renderer, pm, clips, 255, pFlag, childClipper); if (childClipper) clips.push(trd); } } @@ -314,8 +290,9 @@ RenderData Paint::Impl::update(RenderMethod* renderer, const RenderTransform* pT opacity = MULTIPLY(opacity, this->opacity); RenderData rd = nullptr; - RenderTransform outTransform(pTransform, rTransform); - PAINT_METHOD(rd, update(renderer, &outTransform, clips, opacity, newFlag, clipper)); + + tr.cm = pm * tr.m; + PAINT_METHOD(rd, update(renderer, tr.cm, clips, opacity, newFlag, clipper)); /* 3. Composition Post Processing */ if (compFastTrack == Result::Success) renderer->viewport(viewport); @@ -325,13 +302,13 @@ RenderData Paint::Impl::update(RenderMethod* renderer, const RenderTransform* pT } -bool Paint::Impl::bounds(float* x, float* y, float* w, float* h, bool transformed, bool stroking) +bool Paint::Impl::bounds(float* x, float* y, float* w, float* h, bool transformed, bool stroking, bool origin) { - Matrix* m = nullptr; bool ret; + const auto& m = this->transform(origin); //Case: No transformed, quick return! - if (!transformed || !(m = this->transform())) { + if (!transformed || mathIdentity(&m)) { PAINT_METHOD(ret, bounds(x, y, w, h, stroking)); return ret; } @@ -355,7 +332,7 @@ bool Paint::Impl::bounds(float* x, float* y, float* w, float* h, bool transforme //Compute the AABB after transformation for (int i = 0; i < 4; i++) { - pt[i] *= *m; + pt[i] *= m; if (pt[i].x < x1) x1 = pt[i].x; if (pt[i].x > x2) x2 = pt[i].x; @@ -372,6 +349,26 @@ bool Paint::Impl::bounds(float* x, float* y, float* w, float* h, bool transforme } +void Paint::Impl::reset() +{ + if (compData) { + if (P(compData->target)->unref() == 0) delete(compData->target); + free(compData); + compData = nullptr; + } + mathIdentity(&tr.m); + tr.degree = 0.0f; + tr.scale = 1.0f; + tr.overriding = false; + + blendMethod = BlendMethod::Normal; + renderFlag = RenderUpdateFlag::None; + ctxFlag = ContextFlag::Invalid; + opacity = 255; + paint->id = 0; +} + + /************************************************************************/ /* External Class Implementation */ /************************************************************************/ @@ -417,9 +414,7 @@ Result Paint::transform(const Matrix& m) noexcept Matrix Paint::transform() noexcept { - auto pTransform = pImpl->transform(); - if (pTransform) return *pTransform; - return {1, 0, 0, 0, 1, 0, 0, 0, 1}; + return pImpl->transform(); } @@ -429,9 +424,9 @@ TVG_DEPRECATED Result Paint::bounds(float* x, float* y, float* w, float* h) cons } -Result Paint::bounds(float* x, float* y, float* w, float* h, bool transform) const noexcept +Result Paint::bounds(float* x, float* y, float* w, float* h, bool transformed) const noexcept { - if (pImpl->bounds(x, y, w, h, transform, true)) return Result::Success; + if (pImpl->bounds(x, y, w, h, transformed, true, transformed)) return Result::Success; return Result::InsufficientCondition; } @@ -444,6 +439,11 @@ Paint* Paint::duplicate() const noexcept Result Paint::composite(std::unique_ptr<Paint> target, CompositeMethod method) noexcept { + if (method == CompositeMethod::ClipPath && target && target->identifier() != TVG_CLASS_ID_SHAPE) { + TVGERR("RENDERER", "ClipPath only allows the Shape!"); + return Result::NonSupport; + } + auto p = target.release(); if (pImpl->composite(this, p, method)) return Result::Success; delete(p); @@ -486,7 +486,7 @@ uint32_t Paint::identifier() const noexcept } -Result Paint::blend(BlendMethod method) const noexcept +Result Paint::blend(BlendMethod method) noexcept { if (pImpl->blendMethod != method) { pImpl->blendMethod = method; diff --git a/thirdparty/thorvg/src/renderer/tvgPaint.h b/thirdparty/thorvg/src/renderer/tvgPaint.h index bc07ab52ab..e43ca239bb 100644 --- a/thirdparty/thorvg/src/renderer/tvgPaint.h +++ b/thirdparty/thorvg/src/renderer/tvgPaint.h @@ -48,17 +48,40 @@ namespace tvg struct Paint::Impl { Paint* paint = nullptr; - RenderTransform* rTransform = nullptr; Composite* compData = nullptr; RenderMethod* renderer = nullptr; - BlendMethod blendMethod = BlendMethod::Normal; //uint8_t - uint8_t renderFlag = RenderUpdateFlag::None; - uint8_t ctxFlag = ContextFlag::Invalid; - uint8_t id; - uint8_t opacity = 255; + struct { + Matrix m; //input matrix + Matrix cm; //multipled parents matrix + float degree; //rotation degree + float scale; //scale factor + bool overriding; //user transform? + + void update() + { + if (overriding) return; + m.e11 = 1.0f; + m.e12 = 0.0f; + m.e21 = 0.0f; + m.e22 = 1.0f; + m.e31 = 0.0f; + m.e32 = 0.0f; + m.e33 = 1.0f; + mathScale(&m, scale, scale); + mathRotate(&m, degree); + } + } tr; + BlendMethod blendMethod; + uint8_t renderFlag; + uint8_t ctxFlag; + uint8_t opacity; uint8_t refCnt = 0; //reference count + uint8_t id; //TODO: deprecated, remove it - Impl(Paint* pnt) : paint(pnt) {} + Impl(Paint* pnt) : paint(pnt) + { + reset(); + } ~Impl() { @@ -66,7 +89,6 @@ namespace tvg if (P(compData->target)->unref() == 0) delete(compData->target); free(compData); } - delete(rTransform); if (renderer && (renderer->unref() == 0)) delete(renderer); } @@ -84,23 +106,19 @@ namespace tvg bool transform(const Matrix& m) { - if (!rTransform) { - if (mathIdentity(&m)) return true; - rTransform = new RenderTransform(); - } - rTransform->override(m); + tr.m = m; + tr.overriding = true; renderFlag |= RenderUpdateFlag::Transform; return true; } - Matrix* transform() + Matrix& transform(bool origin = false) { - if (rTransform) { - if (renderFlag & RenderUpdateFlag::Transform) rTransform->update(); - return &rTransform->m; - } - return nullptr; + //update transform + if (renderFlag & RenderUpdateFlag::Transform) tr.update(); + if (origin) return tr.cm; + return tr.m; } bool composite(Paint* source, Paint* target, CompositeMethod method) @@ -135,10 +153,11 @@ namespace tvg bool rotate(float degree); bool scale(float factor); bool translate(float x, float y); - bool bounds(float* x, float* y, float* w, float* h, bool transformed, bool stroking); - RenderData update(RenderMethod* renderer, const RenderTransform* pTransform, Array<RenderData>& clips, uint8_t opacity, RenderUpdateFlag pFlag, bool clipper = false); + bool bounds(float* x, float* y, float* w, float* h, bool transformed, bool stroking, bool origin = false); + RenderData update(RenderMethod* renderer, const Matrix& pm, Array<RenderData>& clips, uint8_t opacity, RenderUpdateFlag pFlag, bool clipper = false); bool render(RenderMethod* renderer); - Paint* duplicate(); + Paint* duplicate(Paint* ret = nullptr); + void reset(); }; } diff --git a/thirdparty/thorvg/src/renderer/tvgPicture.cpp b/thirdparty/thorvg/src/renderer/tvgPicture.cpp index 5bd55a3f7b..e6653b1c99 100644 --- a/thirdparty/thorvg/src/renderer/tvgPicture.cpp +++ b/thirdparty/thorvg/src/renderer/tvgPicture.cpp @@ -73,6 +73,8 @@ bool Picture::Impl::needComposition(uint8_t opacity) bool Picture::Impl::render(RenderMethod* renderer) { bool ret = false; + renderer->blend(picture->blend(), true); + if (surface) return renderer->renderImage(rd); else if (paint) { Compositor* cmp = nullptr; @@ -104,21 +106,6 @@ RenderRegion Picture::Impl::bounds(RenderMethod* renderer) } -RenderTransform Picture::Impl::resizeTransform(const RenderTransform* pTransform) -{ - //Overriding Transformation by the desired image size - auto sx = w / loader->w; - auto sy = h / loader->h; - auto scale = sx < sy ? sx : sy; - - RenderTransform tmp; - tmp.m = {scale, 0, 0, 0, scale, 0, 0, 0, 1}; - - if (!pTransform) return tmp; - else return RenderTransform(pTransform, &tmp); -} - - Result Picture::Impl::load(ImageLoader* loader) { //Same resource has been loaded. @@ -215,18 +202,24 @@ Result Picture::size(float* w, float* h) const noexcept } -Result Picture::mesh(const Polygon* triangles, uint32_t triangleCnt) noexcept +const Paint* Picture::paint(uint32_t id) noexcept { - if (!triangles && triangleCnt > 0) return Result::InvalidArguments; - if (triangles && triangleCnt == 0) return Result::InvalidArguments; - - pImpl->mesh(triangles, triangleCnt); - return Result::Success; -} + struct Value + { + uint32_t id; + const Paint* ret; + } value = {id, nullptr}; + auto cb = [](const tvg::Paint* paint, void* data) -> bool + { + auto p = static_cast<Value*>(data); + if (p->id == paint->id) { + p->ret = paint; + return false; + } + return true; + }; -uint32_t Picture::mesh(const Polygon** triangles) const noexcept -{ - if (triangles) *triangles = pImpl->rm.triangles; - return pImpl->rm.triangleCnt; + tvg::Accessor::gen()->set(this, cb, &value); + return value.ret; } diff --git a/thirdparty/thorvg/src/renderer/tvgPicture.h b/thirdparty/thorvg/src/renderer/tvgPicture.h index bd7021218a..3a4880caba 100644 --- a/thirdparty/thorvg/src/renderer/tvgPicture.h +++ b/thirdparty/thorvg/src/renderer/tvgPicture.h @@ -63,12 +63,10 @@ struct Picture::Impl Surface* surface = nullptr; //bitmap picture uses RenderData rd = nullptr; //engine data float w = 0, h = 0; - RenderMesh rm; //mesh data Picture* picture = nullptr; bool resizing = false; bool needComp = false; //need composition - RenderTransform resizeTransform(const RenderTransform* pTransform); bool needComposition(uint8_t opacity); bool render(RenderMethod* renderer); bool size(float w, float h); @@ -90,58 +88,37 @@ struct Picture::Impl delete(paint); } - RenderData update(RenderMethod* renderer, const RenderTransform* pTransform, Array<RenderData>& clips, uint8_t opacity, RenderUpdateFlag pFlag, bool clipper) + RenderData update(RenderMethod* renderer, const Matrix& transform, Array<RenderData>& clips, uint8_t opacity, RenderUpdateFlag pFlag, TVG_UNUSED bool clipper) { auto flag = static_cast<RenderUpdateFlag>(pFlag | load()); if (surface) { if (flag == RenderUpdateFlag::None) return rd; - auto transform = resizeTransform(pTransform); - rd = renderer->prepare(surface, &rm, rd, &transform, clips, opacity, flag); + + //Overriding Transformation by the desired image size + auto sx = w / loader->w; + auto sy = h / loader->h; + auto scale = sx < sy ? sx : sy; + auto m = transform * Matrix{scale, 0, 0, 0, scale, 0, 0, 0, 1}; + + rd = renderer->prepare(surface, rd, m, clips, opacity, flag); } else if (paint) { if (resizing) { loader->resize(paint, w, h); resizing = false; } needComp = needComposition(opacity) ? true : false; - rd = paint->pImpl->update(renderer, pTransform, clips, opacity, flag, clipper); + rd = paint->pImpl->update(renderer, transform, clips, opacity, flag, false); } return rd; } bool bounds(float* x, float* y, float* w, float* h, bool stroking) { - if (rm.triangleCnt > 0) { - auto triangles = rm.triangles; - auto min = triangles[0].vertex[0].pt; - auto max = triangles[0].vertex[0].pt; - - for (uint32_t i = 0; i < rm.triangleCnt; ++i) { - if (triangles[i].vertex[0].pt.x < min.x) min.x = triangles[i].vertex[0].pt.x; - else if (triangles[i].vertex[0].pt.x > max.x) max.x = triangles[i].vertex[0].pt.x; - if (triangles[i].vertex[0].pt.y < min.y) min.y = triangles[i].vertex[0].pt.y; - else if (triangles[i].vertex[0].pt.y > max.y) max.y = triangles[i].vertex[0].pt.y; - - if (triangles[i].vertex[1].pt.x < min.x) min.x = triangles[i].vertex[1].pt.x; - else if (triangles[i].vertex[1].pt.x > max.x) max.x = triangles[i].vertex[1].pt.x; - if (triangles[i].vertex[1].pt.y < min.y) min.y = triangles[i].vertex[1].pt.y; - else if (triangles[i].vertex[1].pt.y > max.y) max.y = triangles[i].vertex[1].pt.y; - - if (triangles[i].vertex[2].pt.x < min.x) min.x = triangles[i].vertex[2].pt.x; - else if (triangles[i].vertex[2].pt.x > max.x) max.x = triangles[i].vertex[2].pt.x; - if (triangles[i].vertex[2].pt.y < min.y) min.y = triangles[i].vertex[2].pt.y; - else if (triangles[i].vertex[2].pt.y > max.y) max.y = triangles[i].vertex[2].pt.y; - } - if (x) *x = min.x; - if (y) *y = min.y; - if (w) *w = max.x - min.x; - if (h) *h = max.y - min.y; - } else { - if (x) *x = 0; - if (y) *y = 0; - if (w) *w = this->w; - if (h) *h = this->h; - } + if (x) *x = 0; + if (y) *y = 0; + if (w) *w = this->w; + if (h) *h = this->h; return true; } @@ -176,32 +153,21 @@ struct Picture::Impl return load(loader); } - void mesh(const Polygon* triangles, const uint32_t triangleCnt) + Paint* duplicate(Paint* ret) { - if (triangles && triangleCnt > 0) { - this->rm.triangleCnt = triangleCnt; - this->rm.triangles = (Polygon*)malloc(sizeof(Polygon) * triangleCnt); - memcpy(this->rm.triangles, triangles, sizeof(Polygon) * triangleCnt); - } else { - free(this->rm.triangles); - this->rm.triangles = nullptr; - this->rm.triangleCnt = 0; - } - } + if (ret) TVGERR("RENDERER", "TODO: duplicate()"); - Paint* duplicate() - { load(); - auto ret = Picture::gen().release(); - auto dup = ret->pImpl; + auto picture = Picture::gen().release(); + auto dup = picture->pImpl; if (paint) dup->paint = paint->duplicate(); if (loader) { dup->loader = loader; ++dup->loader->sharing; - PP(ret)->renderFlag |= RenderUpdateFlag::Image; + PP(picture)->renderFlag |= RenderUpdateFlag::Image; } dup->surface = surface; @@ -209,13 +175,7 @@ struct Picture::Impl dup->h = h; dup->resizing = resizing; - if (rm.triangleCnt > 0) { - dup->rm.triangleCnt = rm.triangleCnt; - dup->rm.triangles = (Polygon*)malloc(sizeof(Polygon) * rm.triangleCnt); - memcpy(dup->rm.triangles, rm.triangles, sizeof(Polygon) * rm.triangleCnt); - } - - return ret; + return picture; } Iterator* iterator() diff --git a/thirdparty/thorvg/src/renderer/tvgRender.cpp b/thirdparty/thorvg/src/renderer/tvgRender.cpp index 82145b9aa4..8ee76493c2 100644 --- a/thirdparty/thorvg/src/renderer/tvgRender.cpp +++ b/thirdparty/thorvg/src/renderer/tvgRender.cpp @@ -46,41 +46,6 @@ uint32_t RenderMethod::unref() } -void RenderTransform::override(const Matrix& m) -{ - this->m = m; - overriding = true; -} - - -void RenderTransform::update() -{ - if (overriding) return; - - m.e11 = 1.0f; - m.e12 = 0.0f; - - m.e21 = 0.0f; - m.e22 = 1.0f; - - m.e31 = 0.0f; - m.e32 = 0.0f; - m.e33 = 1.0f; - - mathScale(&m, scale, scale); - mathRotate(&m, degree); -} - - -RenderTransform::RenderTransform(const RenderTransform* lhs, const RenderTransform* rhs) -{ - if (lhs && rhs) m = lhs->m * rhs->m; - else if (lhs) m = lhs->m; - else if (rhs) m = rhs->m; - else mathIdentity(&m); -} - - void RenderRegion::intersect(const RenderRegion& rhs) { auto x1 = x + w; diff --git a/thirdparty/thorvg/src/renderer/tvgRender.h b/thirdparty/thorvg/src/renderer/tvgRender.h index ff55748033..f1042884ec 100644 --- a/thirdparty/thorvg/src/renderer/tvgRender.h +++ b/thirdparty/thorvg/src/renderer/tvgRender.h @@ -23,6 +23,7 @@ #ifndef _TVG_RENDER_H_ #define _TVG_RENDER_H_ +#include <math.h> #include "tvgCommon.h" #include "tvgArray.h" #include "tvgLock.h" @@ -85,15 +86,15 @@ struct Compositor uint8_t opacity; }; -struct RenderMesh +struct Vertex { - Polygon* triangles = nullptr; - uint32_t triangleCnt = 0; + Point pt; + Point uv; +}; - ~RenderMesh() - { - free(triangles); - } +struct Polygon +{ + Vertex vertex[3]; }; struct RenderRegion @@ -110,24 +111,6 @@ struct RenderRegion } }; -struct RenderTransform -{ - Matrix m; - float degree = 0.0f; //rotation degree - float scale = 1.0f; //scale factor - bool overriding = false; //user transform? - - void update(); - void override(const Matrix& m); - - RenderTransform() - { - m.e13 = m.e23 = 0.0f; - } - - RenderTransform(const RenderTransform* lhs, const RenderTransform* rhs); -}; - struct RenderStroke { float width = 0.0f; @@ -147,6 +130,57 @@ struct RenderStroke bool simultaneous = true; } trim; + void operator=(const RenderStroke& rhs) + { + width = rhs.width; + + memcpy(color, rhs.color, sizeof(color)); + + delete(fill); + if (rhs.fill) fill = rhs.fill->duplicate(); + else fill = nullptr; + + free(dashPattern); + if (rhs.dashCnt > 0) { + dashPattern = static_cast<float*>(malloc(sizeof(float) * rhs.dashCnt)); + memcpy(dashPattern, rhs.dashPattern, sizeof(float) * rhs.dashCnt); + } else { + dashPattern = nullptr; + } + dashCnt = rhs.dashCnt; + miterlimit = rhs.miterlimit; + cap = rhs.cap; + join = rhs.join; + strokeFirst = rhs.strokeFirst; + trim = rhs.trim; + } + + bool strokeTrim(float& begin, float& end) const + { + begin = trim.begin; + end = trim.end; + + if (fabsf(end - begin) >= 1.0f) { + begin = 0.0f; + end = 1.0f; + return false; + } + + auto loop = true; + + if (begin > 1.0f && end > 1.0f) loop = false; + if (begin < 0.0f && end < 0.0f) loop = false; + if (begin >= 0.0f && begin <= 1.0f && end >= 0.0f && end <= 1.0f) loop = false; + + if (begin > 1.0f) begin -= 1.0f; + if (begin < 0.0f) begin += 1.0f; + if (end > 1.0f) end -= 1.0f; + if (end < 0.0f) end += 1.0f; + + if ((loop && begin < end) || (!loop && begin > end)) std::swap(begin, end); + return true; + } + ~RenderStroke() { free(dashPattern); @@ -191,7 +225,7 @@ struct RenderShape { if (!stroke) return false; if (stroke->trim.begin == 0.0f && stroke->trim.end == 1.0f) return false; - if (stroke->trim.begin == 1.0f && stroke->trim.end == 0.0f) return false; + if (fabsf(stroke->trim.end - stroke->trim.begin) >= 1.0f) return false; return true; } @@ -252,9 +286,8 @@ public: uint32_t unref(); virtual ~RenderMethod() {} - virtual RenderData prepare(const RenderShape& rshape, RenderData data, const RenderTransform* transform, Array<RenderData>& clips, uint8_t opacity, RenderUpdateFlag flags, bool clipper) = 0; - virtual RenderData prepare(const Array<RenderData>& scene, RenderData data, const RenderTransform* transform, Array<RenderData>& clips, uint8_t opacity, RenderUpdateFlag flags) = 0; - virtual RenderData prepare(Surface* surface, const RenderMesh* mesh, RenderData data, const RenderTransform* transform, Array<RenderData>& clips, uint8_t opacity, RenderUpdateFlag flags) = 0; + virtual RenderData prepare(const RenderShape& rshape, RenderData data, const Matrix& transform, Array<RenderData>& clips, uint8_t opacity, RenderUpdateFlag flags, bool clipper) = 0; + virtual RenderData prepare(Surface* surface, RenderData data, const Matrix& transform, Array<RenderData>& clips, uint8_t opacity, RenderUpdateFlag flags) = 0; virtual bool preRender() = 0; virtual bool renderShape(RenderData data) = 0; virtual bool renderImage(RenderData data) = 0; @@ -263,7 +296,7 @@ public: virtual RenderRegion region(RenderData data) = 0; virtual RenderRegion viewport() = 0; virtual bool viewport(const RenderRegion& vp) = 0; - virtual bool blend(BlendMethod method) = 0; + virtual bool blend(BlendMethod method, bool direct = false) = 0; virtual ColorSpace colorSpace() = 0; virtual const Surface* mainSurface() = 0; @@ -288,6 +321,8 @@ static inline bool MASK_REGION_MERGING(CompositeMethod method) //these might expand the rendering region case CompositeMethod::AddMask: case CompositeMethod::DifferenceMask: + case CompositeMethod::LightenMask: + case CompositeMethod::DarkenMask: return true; default: TVGERR("RENDERER", "Unsupported Composite Method! = %d", (int)method); @@ -321,6 +356,8 @@ static inline ColorSpace COMPOSITE_TO_COLORSPACE(RenderMethod* renderer, Composi case CompositeMethod::DifferenceMask: case CompositeMethod::SubtractMask: case CompositeMethod::IntersectMask: + case CompositeMethod::LightenMask: + case CompositeMethod::DarkenMask: return ColorSpace::Grayscale8; //TODO: Optimize Luma/InvLuma colorspace to Grayscale8 case CompositeMethod::LumaMask: diff --git a/thirdparty/thorvg/src/renderer/tvgScene.h b/thirdparty/thorvg/src/renderer/tvgScene.h index 8b1981edfa..190ecd31b9 100644 --- a/thirdparty/thorvg/src/renderer/tvgScene.h +++ b/thirdparty/thorvg/src/renderer/tvgScene.h @@ -101,7 +101,7 @@ struct Scene::Impl return true; } - RenderData update(RenderMethod* renderer, const RenderTransform* transform, Array<RenderData>& clips, uint8_t opacity, RenderUpdateFlag flag, bool clipper) + RenderData update(RenderMethod* renderer, const Matrix& transform, Array<RenderData>& clips, uint8_t opacity, RenderUpdateFlag flag, TVG_UNUSED bool clipper) { if ((needComp = needComposition(opacity))) { /* Overriding opacity value. If this scene is half-translucent, @@ -109,20 +109,10 @@ struct Scene::Impl this->opacity = opacity; opacity = 255; } - - if (clipper) { - Array<RenderData> rds(paints.size()); - for (auto paint : paints) { - rds.push(paint->pImpl->update(renderer, transform, clips, opacity, flag, true)); - } - rd = renderer->prepare(rds, rd, transform, clips, opacity, flag); - return rd; - } else { - for (auto paint : paints) { - paint->pImpl->update(renderer, transform, clips, opacity, flag, false); - } - return nullptr; + for (auto paint : paints) { + paint->pImpl->update(renderer, transform, clips, opacity, flag, false); } + return nullptr; } bool render(RenderMethod* renderer) @@ -130,6 +120,8 @@ struct Scene::Impl Compositor* cmp = nullptr; auto ret = true; + renderer->blend(scene->blend()); + if (needComp) { cmp = renderer->target(bounds(renderer), renderer->colorSpace()); renderer->beginComposite(cmp, CompositeMethod::None, opacity); @@ -198,10 +190,12 @@ struct Scene::Impl return true; } - Paint* duplicate() + Paint* duplicate(Paint* ret) { - auto ret = Scene::gen().release(); - auto dup = ret->pImpl; + if (ret) TVGERR("RENDERER", "TODO: duplicate()"); + + auto scene = Scene::gen().release(); + auto dup = scene->pImpl; for (auto paint : paints) { auto cdup = paint->duplicate(); @@ -209,7 +203,7 @@ struct Scene::Impl dup->paints.push_back(cdup); } - return ret; + return scene; } void clear(bool free) diff --git a/thirdparty/thorvg/src/renderer/tvgShape.h b/thirdparty/thorvg/src/renderer/tvgShape.h index ebc0b304ab..94704aee67 100644 --- a/thirdparty/thorvg/src/renderer/tvgShape.h +++ b/thirdparty/thorvg/src/renderer/tvgShape.h @@ -34,6 +34,7 @@ struct Shape::Impl RenderData rd = nullptr; //engine data Shape* shape; uint8_t flag = RenderUpdateFlag::None; + uint8_t opacity; //for composition bool needComp = false; //composite or not @@ -53,10 +54,13 @@ struct Shape::Impl Compositor* cmp = nullptr; bool ret; + renderer->blend(shape->blend(), !needComp); + if (needComp) { cmp = renderer->target(bounds(renderer), renderer->colorSpace()); renderer->beginComposite(cmp, CompositeMethod::None, opacity); } + ret = renderer->renderShape(rd); if (cmp) renderer->endComposite(cmp); return ret; @@ -95,7 +99,7 @@ struct Shape::Impl return true; } - RenderData update(RenderMethod* renderer, const RenderTransform* transform, Array<RenderData>& clips, uint8_t opacity, RenderUpdateFlag pFlag, bool clipper) + RenderData update(RenderMethod* renderer, const Matrix& transform, Array<RenderData>& clips, uint8_t opacity, RenderUpdateFlag pFlag, bool clipper) { if (static_cast<RenderUpdateFlag>(pFlag | flag) == RenderUpdateFlag::None) return rd; @@ -216,23 +220,6 @@ struct Shape::Impl if (mathEqual(rs.stroke->trim.begin, begin) && mathEqual(rs.stroke->trim.end, end) && rs.stroke->trim.simultaneous == simultaneous) return; - auto loop = true; - - if (begin > 1.0f && end > 1.0f) loop = false; - if (begin < 0.0f && end < 0.0f) loop = false; - if (begin >= 0.0f && begin <= 1.0f && end >= 0.0f && end <= 1.0f) loop = false; - - if (begin > 1.0f) begin -= 1.0f; - if (begin < 0.0f) begin += 1.0f; - if (end > 1.0f) end -= 1.0f; - if (end < 0.0f) end += 1.0f; - - if ((loop && begin < end) || (!loop && begin > end)) { - auto tmp = begin; - begin = end; - end = tmp; - } - rs.stroke->trim.begin = begin; rs.stroke->trim.end = end; rs.stroke->trim.simultaneous = simultaneous; @@ -359,47 +346,56 @@ struct Shape::Impl this->flag |= flag; } - Paint* duplicate() + Paint* duplicate(Paint* ret) { - auto ret = Shape::gen().release(); - auto dup = ret->pImpl; + auto shape = static_cast<Shape*>(ret); + if (shape) shape->reset(); + else shape = Shape::gen().release(); + + auto dup = shape->pImpl; + delete(dup->rs.fill); + //Default Properties + dup->flag = RenderUpdateFlag::All; dup->rs.rule = rs.rule; //Color memcpy(dup->rs.color, rs.color, sizeof(rs.color)); - dup->flag = RenderUpdateFlag::Color; //Path - if (rs.path.cmds.count > 0 && rs.path.pts.count > 0) { - dup->rs.path.cmds = rs.path.cmds; - dup->rs.path.pts = rs.path.pts; - dup->flag |= RenderUpdateFlag::Path; - } + dup->rs.path.cmds.push(rs.path.cmds); + dup->rs.path.pts.push(rs.path.pts); //Stroke if (rs.stroke) { - dup->rs.stroke = new RenderStroke(); + if (!dup->rs.stroke) dup->rs.stroke = new RenderStroke; *dup->rs.stroke = *rs.stroke; - memcpy(dup->rs.stroke->color, rs.stroke->color, sizeof(rs.stroke->color)); - if (rs.stroke->dashCnt > 0) { - dup->rs.stroke->dashPattern = static_cast<float*>(malloc(sizeof(float) * rs.stroke->dashCnt)); - memcpy(dup->rs.stroke->dashPattern, rs.stroke->dashPattern, sizeof(float) * rs.stroke->dashCnt); - } - if (rs.stroke->fill) { - dup->rs.stroke->fill = rs.stroke->fill->duplicate(); - dup->flag |= RenderUpdateFlag::GradientStroke; - } - dup->flag |= RenderUpdateFlag::Stroke; + } else { + delete(dup->rs.stroke); + dup->rs.stroke = nullptr; } //Fill - if (rs.fill) { - dup->rs.fill = rs.fill->duplicate(); - dup->flag |= RenderUpdateFlag::Gradient; - } + if (rs.fill) dup->rs.fill = rs.fill->duplicate(); + else dup->rs.fill = nullptr; - return ret; + return shape; + } + + void reset() + { + PP(shape)->reset(); + rs.path.cmds.clear(); + rs.path.pts.clear(); + + rs.color[3] = 0; + rs.rule = FillRule::Winding; + + delete(rs.stroke); + rs.stroke = nullptr; + + delete(rs.fill); + rs.fill = nullptr; } Iterator* iterator() diff --git a/thirdparty/thorvg/src/renderer/tvgTaskScheduler.h b/thirdparty/thorvg/src/renderer/tvgTaskScheduler.h index 93f8481707..58918e88f0 100644 --- a/thirdparty/thorvg/src/renderer/tvgTaskScheduler.h +++ b/thirdparty/thorvg/src/renderer/tvgTaskScheduler.h @@ -23,8 +23,6 @@ #ifndef _TVG_TASK_SCHEDULER_H_ #define _TVG_TASK_SCHEDULER_H_ -#define _DISABLE_CONSTEXPR_MUTEX_CONSTRUCTOR - #include <mutex> #include <condition_variable> diff --git a/thirdparty/thorvg/src/renderer/tvgText.cpp b/thirdparty/thorvg/src/renderer/tvgText.cpp index 4b5eb35ce5..d54c78783c 100644 --- a/thirdparty/thorvg/src/renderer/tvgText.cpp +++ b/thirdparty/thorvg/src/renderer/tvgText.cpp @@ -35,7 +35,7 @@ /************************************************************************/ -Text::Text() : pImpl(new Impl) +Text::Text() : pImpl(new Impl(this)) { Paint::pImpl->id = TVG_CLASS_ID_TEXT; } @@ -95,20 +95,13 @@ Result Text::unload(const std::string& path) noexcept Result Text::fill(uint8_t r, uint8_t g, uint8_t b) noexcept { - if (!pImpl->paint) return Result::InsufficientCondition; - - return pImpl->fill(r, g, b); + return pImpl->shape->fill(r, g, b); } Result Text::fill(unique_ptr<Fill> f) noexcept { - if (!pImpl->paint) return Result::InsufficientCondition; - - auto p = f.release(); - if (!p) return Result::MemoryCorruption; - - return pImpl->fill(p); + return pImpl->shape->fill(std::move(f)); } diff --git a/thirdparty/thorvg/src/renderer/tvgText.h b/thirdparty/thorvg/src/renderer/tvgText.h index c56ce8b878..746b85bea6 100644 --- a/thirdparty/thorvg/src/renderer/tvgText.h +++ b/thirdparty/thorvg/src/renderer/tvgText.h @@ -26,37 +26,27 @@ #include <cstring> #include "tvgShape.h" #include "tvgFill.h" - -#ifdef THORVG_TTF_LOADER_SUPPORT - #include "tvgTtfLoader.h" -#else - #include "tvgLoader.h" -#endif +#include "tvgLoader.h" struct Text::Impl { FontLoader* loader = nullptr; - Shape* paint = nullptr; + Text* paint; + Shape* shape; char* utf8 = nullptr; float fontSize; bool italic = false; bool changed = false; - ~Impl() - { - free(utf8); - LoaderMgr::retrieve(loader); - delete(paint); - } - - Result fill(uint8_t r, uint8_t g, uint8_t b) + Impl(Text* p) : paint(p), shape(Shape::gen().release()) { - return paint->fill(r, g, b); } - Result fill(Fill* f) + ~Impl() { - return paint->fill(cast<Fill>(f)); + free(utf8); + LoaderMgr::retrieve(loader); + delete(shape); } Result text(const char* utf8) @@ -74,6 +64,11 @@ struct Text::Impl auto loader = LoaderMgr::loader(name); if (!loader) return Result::InsufficientCondition; + if (style && strstr(style, "italic")) italic = true; + else italic = false; + + fontSize = size; + //Same resource has been loaded. if (this->loader == loader) { this->loader->sharing--; //make it sure the reference counting. @@ -83,50 +78,41 @@ struct Text::Impl } this->loader = static_cast<FontLoader*>(loader); - if (!paint) paint = Shape::gen().release(); - - fontSize = size; - if (style && strstr(style, "italic")) italic = true; changed = true; return Result::Success; } RenderRegion bounds(RenderMethod* renderer) { - if (paint) return P(paint)->bounds(renderer); - else return {0, 0, 0, 0}; + return P(shape)->bounds(renderer); } bool render(RenderMethod* renderer) { - if (paint) return PP(paint)->render(renderer); - return true; + renderer->blend(paint->blend(), true); + return PP(shape)->render(renderer); } bool load() { if (!loader) return false; + loader->request(shape, utf8); //reload if (changed) { - loader->request(paint, utf8, italic); loader->read(); changed = false; } - if (paint) { - loader->resize(paint, fontSize, fontSize); - return true; - } - return false; + return loader->transform(shape, fontSize, italic); } - RenderData update(RenderMethod* renderer, const RenderTransform* transform, Array<RenderData>& clips, uint8_t opacity, RenderUpdateFlag pFlag, bool clipper) + RenderData update(RenderMethod* renderer, const Matrix& transform, Array<RenderData>& clips, uint8_t opacity, RenderUpdateFlag pFlag, TVG_UNUSED bool clipper) { if (!load()) return nullptr; //transform the gradient coordinates based on the final scaled font. - if (P(paint)->flag & RenderUpdateFlag::Gradient) { - auto fill = P(paint)->rs.fill; + auto fill = P(shape)->rs.fill; + if (fill && P(shape)->flag & RenderUpdateFlag::Gradient) { auto scale = 1.0f / loader->scale; if (fill->identifier() == TVG_CLASS_ID_LINEAR) { P(static_cast<LinearGradient*>(fill))->x1 *= scale; @@ -142,23 +128,25 @@ struct Text::Impl P(static_cast<RadialGradient*>(fill))->fr *= scale; } } - return PP(paint)->update(renderer, transform, clips, opacity, pFlag, clipper); + return PP(shape)->update(renderer, transform, clips, opacity, pFlag, false); } bool bounds(float* x, float* y, float* w, float* h, TVG_UNUSED bool stroking) { - if (!load() || !paint) return false; - paint->bounds(x, y, w, h, true); + if (!load()) return false; + PP(shape)->bounds(x, y, w, h, true, true, false); return true; } - Paint* duplicate() + Paint* duplicate(Paint* ret) { + if (ret) TVGERR("RENDERER", "TODO: duplicate()"); + load(); - auto ret = Text::gen().release(); - auto dup = ret->pImpl; - if (paint) dup->paint = static_cast<Shape*>(paint->duplicate()); + auto text = Text::gen().release(); + auto dup = text->pImpl; + P(shape)->duplicate(dup->shape); if (loader) { dup->loader = loader; @@ -169,7 +157,7 @@ struct Text::Impl dup->italic = italic; dup->fontSize = fontSize; - return ret; + return text; } Iterator* iterator() diff --git a/thirdparty/thorvg/update-thorvg.sh b/thirdparty/thorvg/update-thorvg.sh index 1a68daf3c5..51dc156661 100755 --- a/thirdparty/thorvg/update-thorvg.sh +++ b/thirdparty/thorvg/update-thorvg.sh @@ -1,16 +1,22 @@ #!/bin/bash -e -VERSION=0.14.2 +VERSION=0.14.8 +# Uncomment and set a git hash to use specific commit instead of tag. +#GIT_COMMIT= -cd thirdparty/thorvg/ || true +pushd "$(dirname "$0")" rm -rf AUTHORS LICENSE inc/ src/ *.zip *.tar.gz tmp/ mkdir tmp/ && pushd tmp/ # Release -curl -L -O https://github.com/thorvg/thorvg/archive/v$VERSION.tar.gz -# Current Github main branch tip -#curl -L -O https://github.com/thorvg/thorvg/archive/refs/heads/main.tar.gz +if [ ! -z "$GIT_COMMIT" ]; then + echo "Updating ThorVG to commit:" $GIT_COMMIT + curl -L -O https://github.com/thorvg/thorvg/archive/$GIT_COMMIT.tar.gz +else + echo "Updating ThorVG to tagged release:" $VERSION + curl -L -O https://github.com/thorvg/thorvg/archive/v$VERSION.tar.gz +fi tar --strip-components=1 -xvf *.tar.gz rm *.tar.gz @@ -70,4 +76,4 @@ cp -rv src/loaders/jpg ../src/loaders/ popd rm -rf tmp - +popd |