summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.pre-commit-config.yaml9
-rw-r--r--COPYRIGHT.txt3
-rw-r--r--SConstruct9
-rw-r--r--core/config/engine.cpp4
-rw-r--r--core/config/engine.h2
-rw-r--r--core/core_bind.cpp19
-rw-r--r--core/core_bind.h1
-rw-r--r--core/crypto/crypto.cpp24
-rw-r--r--core/crypto/crypto.h16
-rw-r--r--core/error/error_list.h1
-rw-r--r--core/extension/gdextension.cpp417
-rw-r--r--core/extension/gdextension.h29
-rw-r--r--core/extension/gdextension_interface.cpp10
-rw-r--r--core/extension/gdextension_interface.h78
-rw-r--r--core/extension/gdextension_library_loader.cpp390
-rw-r--r--core/extension/gdextension_library_loader.h84
-rw-r--r--core/extension/gdextension_loader.h (renamed from platform/android/java/lib/src/org/godotengine/godot/io/file/FileErrors.kt)38
-rw-r--r--core/extension/gdextension_manager.cpp19
-rw-r--r--core/extension/gdextension_manager.h1
-rw-r--r--core/io/dtls_server.cpp6
-rw-r--r--core/io/dtls_server.h4
-rw-r--r--core/io/file_access.cpp8
-rw-r--r--core/io/http_client.cpp4
-rw-r--r--core/io/http_client.h4
-rw-r--r--core/io/http_client_tcp.cpp6
-rw-r--r--core/io/http_client_tcp.h2
-rw-r--r--core/io/ip.cpp33
-rw-r--r--core/io/packet_peer_dtls.cpp6
-rw-r--r--core/io/packet_peer_dtls.h4
-rw-r--r--core/io/resource.cpp25
-rw-r--r--core/io/resource.h2
-rw-r--r--core/io/resource_importer.cpp17
-rw-r--r--core/io/resource_importer.h1
-rw-r--r--core/io/resource_loader.cpp333
-rw-r--r--core/io/resource_loader.h16
-rw-r--r--core/io/stream_peer_tls.cpp6
-rw-r--r--core/io/stream_peer_tls.h4
-rw-r--r--core/object/class_db.cpp116
-rw-r--r--core/object/class_db.h21
-rw-r--r--core/object/object.cpp8
-rw-r--r--core/object/object.h4
-rw-r--r--core/object/worker_thread_pool.cpp67
-rw-r--r--core/object/worker_thread_pool.h20
-rw-r--r--core/os/condition_variable.h8
-rw-r--r--core/os/mutex.h24
-rw-r--r--core/os/safe_binary_mutex.h95
-rw-r--r--core/string/node_path.cpp10
-rw-r--r--core/string/ustring.cpp77
-rw-r--r--core/templates/command_queue_mt.cpp8
-rw-r--r--core/templates/command_queue_mt.h18
-rw-r--r--core/templates/paged_allocator.h24
-rw-r--r--core/templates/rid_owner.h40
-rw-r--r--core/templates/sort_array.h6
-rw-r--r--doc/classes/@GlobalScope.xml12
-rw-r--r--doc/classes/AStar2D.xml4
-rw-r--r--doc/classes/AStar3D.xml4
-rw-r--r--doc/classes/AnimatedSprite2D.xml6
-rw-r--r--doc/classes/AnimatedSprite3D.xml6
-rw-r--r--doc/classes/AnimationNodeStateMachine.xml1
-rw-r--r--doc/classes/AnimationNodeStateMachinePlayback.xml1
-rw-r--r--doc/classes/Array.xml7
-rw-r--r--doc/classes/Callable.xml4
-rw-r--r--doc/classes/CallbackTweener.xml4
-rw-r--r--doc/classes/ClassDB.xml8
-rw-r--r--doc/classes/EditorInterface.xml3
-rw-r--r--doc/classes/EditorPlugin.xml10
-rw-r--r--doc/classes/EditorSettings.xml4
-rw-r--r--doc/classes/HTTPClient.xml3
-rw-r--r--doc/classes/Image.xml2
-rw-r--r--doc/classes/Input.xml1
-rw-r--r--doc/classes/InputEventMouseMotion.xml6
-rw-r--r--doc/classes/JSON.xml5
-rw-r--r--doc/classes/JavaScriptObject.xml1
-rw-r--r--doc/classes/LineEdit.xml4
-rw-r--r--doc/classes/LinkButton.xml1
-rw-r--r--doc/classes/MeshDataTool.xml3
-rw-r--r--doc/classes/Object.xml4
-rw-r--r--doc/classes/ProjectSettings.xml14
-rw-r--r--doc/classes/PropertyTweener.xml9
-rw-r--r--doc/classes/RenderingDevice.xml37
-rw-r--r--doc/classes/Resource.xml2
-rw-r--r--doc/classes/RichTextLabel.xml26
-rw-r--r--doc/classes/ScriptEditor.xml1
-rw-r--r--doc/classes/Signal.xml2
-rw-r--r--doc/classes/SpinBox.xml3
-rw-r--r--doc/classes/Sprite2D.xml4
-rw-r--r--doc/classes/String.xml5
-rw-r--r--doc/classes/StringName.xml4
-rw-r--r--doc/classes/StyleBoxFlat.xml3
-rw-r--r--doc/classes/Tween.xml2
-rw-r--r--drivers/d3d12/SCsub1
-rw-r--r--drivers/d3d12/rendering_device_driver_d3d12.h5
-rw-r--r--drivers/egl/egl_manager.cpp4
-rw-r--r--drivers/gles3/shaders/scene.glsl14
-rw-r--r--drivers/gles3/storage/texture_storage.cpp6
-rw-r--r--drivers/metal/metal_objects.h84
-rw-r--r--drivers/metal/metal_objects.mm201
-rw-r--r--drivers/metal/metal_utils.h20
-rw-r--r--drivers/metal/rendering_device_driver_metal.h17
-rw-r--r--drivers/metal/rendering_device_driver_metal.mm116
-rw-r--r--drivers/vulkan/rendering_context_driver_vulkan.cpp27
-rw-r--r--drivers/vulkan/rendering_context_driver_vulkan.h4
-rw-r--r--drivers/vulkan/rendering_device_driver_vulkan.cpp6
-rw-r--r--editor/animation_track_editor.cpp11
-rw-r--r--editor/editor_file_system.cpp82
-rw-r--r--editor/editor_help.cpp6
-rw-r--r--editor/editor_log.cpp2
-rw-r--r--editor/editor_node.cpp95
-rw-r--r--editor/editor_node.h7
-rw-r--r--editor/editor_paths.cpp4
-rw-r--r--editor/editor_settings.cpp1
-rw-r--r--editor/export/editor_export_platform.cpp2
-rw-r--r--editor/export/export_template_manager.cpp6
-rw-r--r--editor/export/export_template_manager.h1
-rw-r--r--editor/export/project_export.cpp14
-rw-r--r--editor/gui/scene_tree_editor.cpp1
-rw-r--r--editor/import/3d/resource_importer_scene.cpp4
-rw-r--r--editor/import/3d/scene_import_settings.cpp1
-rw-r--r--editor/plugins/animation_library_editor.cpp4
-rw-r--r--editor/plugins/font_config_plugin.cpp3
-rw-r--r--editor/plugins/gdextension_export_plugin.h5
-rw-r--r--editor/plugins/material_editor_plugin.cpp33
-rw-r--r--editor/plugins/material_editor_plugin.h5
-rw-r--r--editor/plugins/mesh_instance_3d_editor_plugin.cpp67
-rw-r--r--editor/plugins/mesh_instance_3d_editor_plugin.h3
-rw-r--r--editor/plugins/node_3d_editor_plugin.cpp2
-rw-r--r--editor/plugins/polygon_2d_editor_plugin.cpp3
-rw-r--r--editor/plugins/script_editor_plugin.cpp4
-rw-r--r--editor/plugins/shader_editor_plugin.cpp10
-rw-r--r--editor/plugins/sprite_frames_editor_plugin.cpp90
-rw-r--r--editor/plugins/sprite_frames_editor_plugin.h3
-rw-r--r--editor/plugins/texture_3d_editor_plugin.cpp127
-rw-r--r--editor/plugins/texture_3d_editor_plugin.h8
-rw-r--r--editor/plugins/texture_editor_plugin.cpp7
-rw-r--r--editor/plugins/texture_layered_editor_plugin.cpp241
-rw-r--r--editor/plugins/texture_layered_editor_plugin.h8
-rw-r--r--editor/plugins/visual_shader_editor_plugin.cpp456
-rw-r--r--editor/plugins/visual_shader_editor_plugin.h42
-rw-r--r--main/main.cpp98
-rw-r--r--misc/extension_api_validation/4.1-stable_4.2-stable.expected9
-rw-r--r--misc/extension_api_validation/4.2-stable_4.3-stable.expected9
-rwxr-xr-xmisc/scripts/validate_extension_api.sh4
-rw-r--r--modules/fbx/fbx_document.cpp1
-rw-r--r--modules/gdscript/doc_classes/@GDScript.xml4
-rw-r--r--modules/gdscript/gdscript_cache.cpp10
-rw-r--r--modules/gdscript/gdscript_cache.h9
-rw-r--r--modules/gdscript/gdscript_editor.cpp33
-rw-r--r--modules/lightmapper_rd/lightmapper_rd.cpp17
-rw-r--r--modules/lightmapper_rd/lightmapper_rd.h2
-rw-r--r--modules/lightmapper_rd/lm_compute.glsl30
-rw-r--r--modules/mbedtls/crypto_mbedtls.cpp16
-rw-r--r--modules/mbedtls/crypto_mbedtls.h8
-rw-r--r--modules/mbedtls/dtls_server_mbedtls.cpp4
-rw-r--r--modules/mbedtls/dtls_server_mbedtls.h2
-rw-r--r--modules/mbedtls/packet_peer_mbed_dtls.cpp4
-rw-r--r--modules/mbedtls/packet_peer_mbed_dtls.h2
-rw-r--r--modules/mbedtls/stream_peer_mbedtls.cpp4
-rw-r--r--modules/mbedtls/stream_peer_mbedtls.h2
-rw-r--r--modules/mono/editor/bindings_generator.cpp2
-rw-r--r--modules/mono/glue/GodotSharp/GodotSharp/Core/GodotObject.base.cs7
-rw-r--r--modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/NativeFuncs.cs2
-rw-r--r--modules/mono/glue/runtime_interop.cpp2
-rw-r--r--modules/noise/noise_texture_3d.cpp4
-rw-r--r--modules/noise/noise_texture_3d.h2
-rw-r--r--modules/openxr/extensions/openxr_hand_tracking_extension.cpp2
-rw-r--r--modules/openxr/openxr_api.cpp6
-rw-r--r--modules/upnp/doc_classes/UPNP.xml4
-rw-r--r--modules/webrtc/webrtc_peer_connection.cpp13
-rw-r--r--modules/webrtc/webrtc_peer_connection.h2
-rw-r--r--modules/websocket/emws_peer.h2
-rw-r--r--modules/websocket/websocket_peer.cpp2
-rw-r--r--modules/websocket/websocket_peer.h6
-rw-r--r--modules/websocket/wsl_peer.h2
-rw-r--r--platform/android/dir_access_jandroid.cpp26
-rw-r--r--platform/android/dir_access_jandroid.h2
-rw-r--r--platform/android/export/export.cpp11
-rw-r--r--platform/android/export/export_plugin.cpp92
-rw-r--r--platform/android/file_access_android.h2
-rw-r--r--platform/android/file_access_filesystem_jandroid.cpp26
-rw-r--r--platform/android/file_access_filesystem_jandroid.h2
-rw-r--r--platform/android/java/THIRDPARTY.md (renamed from platform/android/java/lib/THIRDPARTY.md)28
-rw-r--r--platform/android/java/editor/build.gradle1
-rw-r--r--platform/android/java/editor/src/main/assets/keystores/debug.keystorebin0 -> 2714 bytes
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/ApkSigner.java1801
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/ApkSignerEngine.java550
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/ApkVerificationIssue.java173
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/ApkVerifier.java3657
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/Constants.java65
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/DefaultApkSignerEngine.java2241
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/Hints.java123
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/README.md32
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/SigningCertificateLineage.java1325
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/SourceStampVerifier.java911
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/apk/ApkFormatException.java35
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/apk/ApkSigningBlockNotFoundException.java32
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/apk/ApkUtils.java670
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/apk/ApkUtilsLite.java199
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/apk/CodenameMinSdkVersionException.java46
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/apk/MinSdkVersionException.java40
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/AndroidBinXmlParser.java869
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/ApkSigResult.java104
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/ApkSignerInfo.java104
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtils.java1444
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtilsLite.java393
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/ApkSupportedSignature.java35
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/ContentDigestAlgorithm.java61
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/NoApkSupportedSignaturesException.java27
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/SignatureAlgorithm.java225
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/SignatureInfo.java53
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/SignatureNotFoundException.java30
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampCertificateLineage.java235
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampConstants.java34
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampVerifier.java364
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/stamp/V1SourceStampSigner.java109
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/stamp/V1SourceStampVerifier.java139
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/stamp/V2SourceStampSigner.java286
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/stamp/V2SourceStampVerifier.java159
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v1/DigestAlgorithm.java74
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeConstants.java26
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeSigner.java586
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeVerifier.java1570
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeConstants.java25
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeSigner.java329
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeVerifier.java471
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeConstants.java66
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeSigner.java531
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeVerifier.java783
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v3/V3SigningCertificateLineage.java314
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeSigner.java440
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeVerifier.java267
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/apk/v4/V4Signature.java311
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1BerParser.java673
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1Class.java28
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1DecodingException.java32
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1DerEncoder.java596
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1EncodingException.java32
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1Field.java45
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1OpaqueObject.java38
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1TagClass.java30
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1Tagging.java23
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/Asn1Type.java35
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/ber/BerDataValue.java115
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/ber/BerDataValueFormatException.java34
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/ber/BerDataValueReader.java34
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/ber/BerEncoding.java225
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/ber/ByteBufferBerDataValueReader.java208
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/asn1/ber/InputStreamBerDataValueReader.java313
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/jar/ManifestParser.java363
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/jar/ManifestWriter.java127
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/jar/SignatureFileWriter.java61
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/oid/OidConstants.java463
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/AlgorithmIdentifier.java173
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/Attribute.java36
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/ContentInfo.java36
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/EncapsulatedContentInfo.java46
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/IssuerAndSerialNumber.java43
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/Pkcs7Constants.java29
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/Pkcs7DecodingException.java32
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/SignedData.java58
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/SignerIdentifier.java42
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/pkcs7/SignerInfo.java61
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/util/AndroidSdkVersion.java74
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/util/ByteArrayDataSink.java240
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/util/ByteBufferDataSource.java125
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/util/ByteBufferSink.java59
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/util/ByteBufferUtils.java33
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/util/ByteStreams.java41
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/util/ChainedDataSource.java145
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/util/DelegatingX509Certificate.java219
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/util/FileChannelDataSource.java191
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/util/GuaranteedEncodedFormX509Certificate.java68
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/util/InclusiveIntRange.java89
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/util/MessageDigestSink.java51
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/util/OutputStreamDataSink.java77
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/util/Pair.java81
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/util/RandomAccessFileDataSink.java104
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/util/TeeDataSink.java51
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/util/VerityTreeBuilder.java325
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/util/X509CertificateUtils.java282
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/x509/AttributeTypeAndValue.java35
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/x509/Certificate.java105
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/x509/Extension.java38
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/x509/Name.java34
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/x509/RSAPublicKey.java35
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/x509/RelativeDistinguishedName.java33
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/x509/SubjectPublicKeyInfo.java36
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/x509/TBSCertificate.java79
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/x509/Time.java34
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/x509/Validity.java34
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/zip/CentralDirectoryRecord.java304
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/zip/EocdRecord.java57
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/zip/LocalFileRecord.java543
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/internal/zip/ZipUtils.java385
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/util/DataSink.java46
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/util/DataSinks.java75
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/util/DataSource.java110
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/util/DataSources.java80
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/util/ReadableDataSink.java24
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/util/RunnablesExecutor.java61
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/util/RunnablesProvider.java21
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/zip/ZipFormatException.java32
-rw-r--r--platform/android/java/editor/src/main/java/com/android/apksig/zip/ZipSections.java85
-rw-r--r--platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt19
-rw-r--r--platform/android/java/editor/src/main/java/org/godotengine/editor/utils/ApkSignerUtil.kt204
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/Godot.kt30
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/GodotFragment.java17
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/GodotHost.java28
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/GodotLib.java5
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/error/Error.kt100
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/io/StorageScope.kt29
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/io/directory/AssetsDirectoryAccess.kt47
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/io/directory/DirectoryAccessHandler.kt186
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/io/directory/FilesystemDirectoryAccess.kt6
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/io/file/AssetData.kt151
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/io/file/DataAccess.kt220
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessFlags.kt2
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessHandler.kt65
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/io/file/FileData.kt14
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/io/file/MediaStoreData.kt2
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/utils/BenchmarkUtils.kt5
-rw-r--r--platform/android/java_godot_lib_jni.cpp5
-rw-r--r--platform/android/java_godot_lib_jni.h1
-rw-r--r--platform/android/java_godot_wrapper.cpp41
-rw-r--r--platform/android/java_godot_wrapper.h6
-rw-r--r--platform/android/os_android.cpp10
-rw-r--r--platform/android/os_android.h5
-rw-r--r--platform/linuxbsd/wayland/wayland_thread.h2
-rw-r--r--platform/macos/display_server_macos.mm6
-rw-r--r--platform/web/audio_driver_web.cpp1
-rw-r--r--platform/web/export/export.cpp2
-rw-r--r--platform/web/godot_audio.h2
-rw-r--r--platform/web/http_client_web.cpp6
-rw-r--r--platform/web/http_client_web.h2
-rw-r--r--platform/web/js/libs/library_godot_audio.js10
-rw-r--r--platform/windows/display_server_windows.cpp44
-rw-r--r--scene/3d/xr_hand_modifier_3d.cpp5
-rw-r--r--scene/audio/audio_stream_player_internal.cpp1
-rw-r--r--scene/gui/control.cpp10
-rw-r--r--scene/gui/menu_button.cpp2
-rw-r--r--scene/gui/option_button.cpp2
-rw-r--r--scene/gui/popup_menu.cpp2
-rw-r--r--scene/gui/rich_text_label.cpp9
-rw-r--r--scene/gui/rich_text_label.h2
-rw-r--r--scene/main/viewport.cpp7
-rw-r--r--scene/property_list_helper.cpp2
-rw-r--r--scene/resources/audio_stream_wav.cpp1
-rw-r--r--scene/resources/material.cpp8
-rw-r--r--scene/resources/skeleton_profile.cpp5
-rw-r--r--scene/resources/visual_shader.cpp357
-rw-r--r--scene/resources/visual_shader.h9
-rw-r--r--servers/audio/audio_stream.h1
-rw-r--r--servers/rendering/renderer_rd/shaders/forward_clustered/scene_forward_clustered.glsl14
-rw-r--r--servers/rendering/renderer_rd/shaders/forward_mobile/scene_forward_mobile.glsl15
-rw-r--r--servers/rendering/rendering_context_driver.cpp45
-rw-r--r--servers/rendering/rendering_context_driver.h2
-rw-r--r--servers/rendering/rendering_device.cpp5
-rw-r--r--servers/rendering/rendering_device.h2
-rw-r--r--servers/rendering/shader_language.cpp5
-rw-r--r--tests/core/object/test_class_db.h40
359 files changed, 35670 insertions, 1866 deletions
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..d2cfe2732c 100644
--- a/SConstruct
+++ b/SConstruct
@@ -234,6 +234,8 @@ 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(BoolVariable("verbose", "Enable verbose output for the compilation", False))
opts.Add(BoolVariable("progress", "Show a progress indicator during compilation", True))
@@ -1031,13 +1033,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/config/engine.cpp b/core/config/engine.cpp
index 3574430cf7..9cdc21fe8e 100644
--- a/core/config/engine.cpp
+++ b/core/config/engine.cpp
@@ -263,6 +263,10 @@ bool Engine::is_generate_spirv_debug_info_enabled() const {
return generate_spirv_debug_info;
}
+bool Engine::is_extra_gpu_memory_tracking_enabled() const {
+ return extra_gpu_memory_tracking;
+}
+
void Engine::set_print_error_messages(bool p_enabled) {
CoreGlobals::print_error_enabled = p_enabled;
}
diff --git a/core/config/engine.h b/core/config/engine.h
index 7e617d8773..f858eba328 100644
--- a/core/config/engine.h
+++ b/core/config/engine.h
@@ -72,6 +72,7 @@ private:
bool abort_on_gpu_errors = false;
bool use_validation_layers = false;
bool generate_spirv_debug_info = false;
+ bool extra_gpu_memory_tracking = false;
int32_t gpu_idx = -1;
uint64_t _process_frames = 0;
@@ -181,6 +182,7 @@ public:
bool is_abort_on_gpu_errors_enabled() const;
bool is_validation_layers_enabled() const;
bool is_generate_spirv_debug_info_enabled() const;
+ bool is_extra_gpu_memory_tracking_enabled() const;
int32_t get_gpu_index() const;
void increment_frames_drawn();
diff --git a/core/core_bind.cpp b/core/core_bind.cpp
index 36f662b92b..84f66c3479 100644
--- a/core/core_bind.cpp
+++ b/core/core_bind.cpp
@@ -1501,6 +1501,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 +1640,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);
diff --git a/core/core_bind.h b/core/core_bind.h
index d744da2551..0949ba628f 100644
--- a/core/core_bind.h
+++ b/core/core_bind.h
@@ -460,6 +460,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/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..940a34396f 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,16 +304,53 @@ 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);
@@ -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,34 @@ 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) {
+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;
+
String abs_path = ProjectSettings::get_singleton()->globalize_path(p_path);
- 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));
- }
- }
-
- 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);
+ Error err = loader->open_library(abs_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 +710,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 +729,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 +753,7 @@ GDExtension::GDExtension() {
}
GDExtension::~GDExtension() {
- if (library != nullptr) {
+ if (is_library_open()) {
close_library();
}
#ifdef TOOLS_ENABLED
@@ -888,8 +770,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 +792,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 +841,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..a5a0fc906a 100644
--- a/core/extension/gdextension_interface.cpp
+++ b/core/extension/gdextension_interface.cpp
@@ -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..cac76d39bd 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, bool 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/platform/android/java/lib/src/org/godotengine/godot/io/file/FileErrors.kt b/core/extension/gdextension_loader.h
index 2df0195de7..7d779858b7 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/io/file/FileErrors.kt
+++ b/core/extension/gdextension_loader.h
@@ -1,5 +1,5 @@
/**************************************************************************/
-/* FileErrors.kt */
+/* gdextension_loader.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
@@ -28,26 +28,20 @@
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
-package org.godotengine.godot.io.file
+#ifndef GDEXTENSION_LOADER_H
+#define GDEXTENSION_LOADER_H
-/**
- * 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);
+#include "core/object/ref_counted.h"
- companion object {
- fun fromNativeError(error: Int): FileErrors? {
- for (fileError in entries) {
- if (fileError.nativeValue == error) {
- return fileError
- }
- }
- return null
- }
- }
-}
+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..eeae6b1996 100644
--- a/core/extension/gdextension_manager.cpp
+++ b/core/extension/gdextension_manager.cpp
@@ -31,6 +31,7 @@
#include "gdextension_manager.h"
#include "core/extension/gdextension_compat_hashes.h"
+#include "core/extension/gdextension_library_loader.h"
#include "core/io/file_access.h"
#include "core/object/script_language.h"
@@ -69,11 +70,22 @@ 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;
}
@@ -82,6 +94,7 @@ GDExtensionManager::LoadStatus GDExtensionManager::load_extension(const String &
return status;
}
+ extension->set_path(p_path);
gdextension_map[p_path] = extension;
return LOAD_STATUS_OK;
}
@@ -117,7 +130,7 @@ 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;
}
diff --git a/core/extension/gdextension_manager.h b/core/extension/gdextension_manager.h
index 9386e356bb..b488189604 100644
--- a/core/extension/gdextension_manager.h
+++ b/core/extension/gdextension_manager.h
@@ -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;
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..c857d54925 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) {
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/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/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..598c99c188 100644
--- a/core/io/resource.cpp
+++ b/core/io/resource.cpp
@@ -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
@@ -558,11 +552,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() :
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..b4c43abe00 100644
--- a/core/io/resource_importer.cpp
+++ b/core/io/resource_importer.cpp
@@ -364,6 +364,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..7b1806c3d2 100644
--- a/core/io/resource_importer.h
+++ b/core/io/resource_importer.h
@@ -93,6 +93,7 @@ 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();
};
diff --git a/core/io/resource_loader.cpp b/core/io/resource_loader.cpp
index c9ed4e27d9..d2c4668d12 100644
--- a/core/io/resource_loader.cpp
+++ b/core/io/resource_loader.cpp
@@ -207,34 +207,53 @@ 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;
+ // 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.awaited) {
+ if (load_task.task_id && !load_task.awaited) {
task_to_await = load_task.task_id;
- load_task.awaited = true;
}
+ // 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();
}
- 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
}
}
@@ -295,11 +314,11 @@ 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();
@@ -322,8 +341,10 @@ void ResourceLoader::_thread_load_function(void *p_userdata) {
}
// --
+ bool xl_remapped = false;
+ const String &remapped_path = _path_remap(load_task.local_path, &xl_remapped);
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();
}
@@ -356,27 +377,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 +460,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)) {
- 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, true);
+ return token.is_valid() ? OK : FAILED;
+}
- 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;
+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.");
+ 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 +509,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,6 +522,13 @@ 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()) {
@@ -493,12 +542,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 +562,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 +584,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 +684,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 +701,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 +741,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 +752,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 +1104,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;
+ }
}
}
}
@@ -1156,7 +1208,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 +1227,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 +1356,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/object/class_db.cpp b/core/object/class_db.cpp
index 7d58f7a724..5c793a676f 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, bool 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(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
@@ -525,12 +525,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 +539,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 +621,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 +636,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 +697,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 +717,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 +754,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 +769,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 +800,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) {
diff --git a/core/object/class_db.h b/core/object/class_db.h
index d83feafeee..d6a95b58e2 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>
@@ -292,6 +300,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..161b1e3dbe 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);
}
@@ -2129,6 +2132,7 @@ bool predelete_handler(Object *p_object) {
}
void postinitialize_handler(Object *p_object) {
+ p_object->_initialize();
p_object->_postinitialize();
}
diff --git a/core/object/object.h b/core/object/object.h
index adb50268d2..7307b7ede0 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;
@@ -632,6 +635,7 @@ private:
int _predelete_ok = 0;
ObjectID _instance_id;
bool _predelete();
+ void _initialize();
void _postinitialize();
bool _can_translate = true;
bool _emitting = false;
diff --git a/core/object/worker_thread_pool.cpp b/core/object/worker_thread_pool.cpp
index 56b9fa8475..7fd43c4094 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) {
@@ -428,13 +429,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 +439,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
@@ -665,38 +658,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
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/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/ustring.cpp b/core/string/ustring.cpp
index 2cfc48d395..303998ab50 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;
}
@@ -4897,8 +4915,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/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/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 2ea6aa15bd..cfb7d00861 100644
--- a/doc/classes/AStar2D.xml
+++ b/doc/classes/AStar2D.xml
@@ -169,7 +169,7 @@
astar.ConnectPoints(2, 3, false);
astar.ConnectPoints(4, 3, false);
astar.ConnectPoints(1, 4, false);
- int[] res = astar.GetIdPath(1, 3); // Returns [1, 2, 3]
+ long[] res = astar.GetIdPath(1, 3); // Returns [1, 2, 3]
[/csharp]
[/codeblocks]
If you change the 2nd point's weight to 3, then the result will be [code][1, 4, 3][/code] instead, because now even though the distance is longer, it's "easier" to get through point 4 than through point 2.
@@ -209,7 +209,7 @@
astar.ConnectPoints(1, 2, true);
astar.ConnectPoints(1, 3, true);
- int[] neighbors = astar.GetPointConnections(1); // Returns [2, 3]
+ long[] neighbors = astar.GetPointConnections(1); // Returns [2, 3]
[/csharp]
[/codeblocks]
</description>
diff --git a/doc/classes/AStar3D.xml b/doc/classes/AStar3D.xml
index 281f4edcc1..4448698c32 100644
--- a/doc/classes/AStar3D.xml
+++ b/doc/classes/AStar3D.xml
@@ -197,7 +197,7 @@
astar.ConnectPoints(2, 3, false);
astar.ConnectPoints(4, 3, false);
astar.ConnectPoints(1, 4, false);
- int[] res = astar.GetIdPath(1, 3); // Returns [1, 2, 3]
+ long[] res = astar.GetIdPath(1, 3); // Returns [1, 2, 3]
[/csharp]
[/codeblocks]
If you change the 2nd point's weight to 3, then the result will be [code][1, 4, 3][/code] instead, because now even though the distance is longer, it's "easier" to get through point 4 than through point 2.
@@ -236,7 +236,7 @@
astar.ConnectPoints(1, 2, true);
astar.ConnectPoints(1, 3, true);
- int[] neighbors = astar.GetPointConnections(1); // Returns [2, 3]
+ long[] neighbors = astar.GetPointConnections(1); // Returns [2, 3]
[/csharp]
[/codeblocks]
</description>
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/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 bd0e05f8e0..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"]
@@ -243,7 +242,7 @@
var numbers = [1, 2, 3]
var extra = [4, 5, 6]
numbers.append_array(extra)
- print(nums) # Prints [1, 2, 3, 4, 5, 6]
+ print(numbers) # Prints [1, 2, 3, 4, 5, 6]
[/codeblock]
</description>
</method>
@@ -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] &gt; b[0])
+ my_items.sort_custom(func(a, b): return a[1] &gt; 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/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/EditorInterface.xml b/doc/classes/EditorInterface.xml
index 7187617c4c..178be439c3 100644
--- a/doc/classes/EditorInterface.xml
+++ b/doc/classes/EditorInterface.xml
@@ -301,7 +301,7 @@
<param index="1" name="valid_types" type="StringName[]" default="[]" />
<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]
+ [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():
@@ -322,7 +322,6 @@
<param index="2" name="type_filter" type="PackedInt32Array" default="PackedInt32Array()" />
<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]
[codeblock]
func _ready():
if Engine.is_editor_hint():
diff --git a/doc/classes/EditorPlugin.xml b/doc/classes/EditorPlugin.xml
index 9c1a6f6af6..ed3233b1ae 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.
diff --git a/doc/classes/EditorSettings.xml b/doc/classes/EditorSettings.xml
index 320b119b6a..4bb7149f2f 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()
@@ -701,6 +700,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>
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/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..8a19aa39bf 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,7 +32,7 @@
- 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>
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/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="&quot;&quot;">
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/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&lt;int&gt; _numbers = new();
+ private Godot.Collections.Array&lt;int&gt; _numbers = new();
public override Godot.Collections.Array&lt;Godot.Collections.Dictionary&gt; _GetPropertyList()
{
@@ -173,7 +173,7 @@
if (propertyName.StartsWith("number_"))
{
int index = int.Parse(propertyName.Substring("number_".Length));
- numbers[index] = value.As&lt;int&gt;();
+ _numbers[index] = value.As&lt;int&gt;();
return true;
}
return false;
diff --git a/doc/classes/ProjectSettings.xml b/doc/classes/ProjectSettings.xml
index 252574c419..bf1632965b 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")
@@ -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.
@@ -2526,8 +2520,8 @@
[b]Note:[/b] This setting is implemented only on Linux/X11.
</member>
<member name="rendering/gl_compatibility/fallback_to_native" type="bool" setter="" getter="" default="true">
- If [code]true[/code], the compatibility renderer will fall back to native OpenGL if ANGLE over Metal is not supported.
- [b]Note:[/b] This setting is implemented only on macOS.
+ If [code]true[/code], the compatibility renderer will fall back to native OpenGL if ANGLE is not supported, or ANGLE dynamic libraries aren't found.
+ [b]Note:[/b] This setting is implemented on macOS and Windows.
</member>
<member name="rendering/gl_compatibility/force_angle_on_devices" type="Array" setter="" getter="">
An [Array] of devices which should always use the ANGLE renderer.
@@ -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.
diff --git a/doc/classes/PropertyTweener.xml b/doc/classes/PropertyTweener.xml
index d3875ddfc2..b7aa6947d9 100644
--- a/doc/classes/PropertyTweener.xml
+++ b/doc/classes/PropertyTweener.xml
@@ -14,10 +14,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 +26,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 +48,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/RenderingDevice.xml b/doc/classes/RenderingDevice.xml
index 96c7d0d4d4..ddd52c6835 100644
--- a/doc/classes/RenderingDevice.xml
+++ b/doc/classes/RenderingDevice.xml
@@ -497,7 +497,7 @@
<return type="int" />
<description>
Returns how many allocations the GPU has performed for internal driver structures.
- This is only used by Vulkan in Debug builds and can return 0 when this information is not tracked or unknown.
+ This is only used by Vulkan in debug builds and can return 0 when this information is not tracked or unknown.
</description>
</method>
<method name="get_device_allocs_by_object_type" qualifiers="const">
@@ -506,7 +506,7 @@
<description>
Same as [method get_device_allocation_count] but filtered for a given object type.
The type argument must be in range [code][0; get_tracked_object_type_count - 1][/code]. If [method get_tracked_object_type_count] is 0, then type argument is ignored and always returns 0.
- This is only used by Vulkan in Debug builds and can return 0 when this information is not tracked or unknown.
+ This is only used by Vulkan in debug builds and can return 0 when this information is not tracked or unknown.
</description>
</method>
<method name="get_device_memory_by_object_type" qualifiers="const">
@@ -515,7 +515,7 @@
<description>
Same as [method get_device_total_memory] but filtered for a given object type.
The type argument must be in range [code][0; get_tracked_object_type_count - 1][/code]. If [method get_tracked_object_type_count] is 0, then type argument is ignored and always returns 0.
- This is only used by Vulkan in Debug builds and can return 0 when this information is not tracked or unknown.
+ This is only used by Vulkan in debug builds and can return 0 when this information is not tracked or unknown.
</description>
</method>
<method name="get_device_name" qualifiers="const">
@@ -534,7 +534,7 @@
<return type="int" />
<description>
Returns how much bytes the GPU is using.
- This is only used by Vulkan in Debug builds and can return 0 when this information is not tracked or unknown.
+ This is only used by Vulkan in debug builds and can return 0 when this information is not tracked or unknown.
</description>
</method>
<method name="get_device_vendor_name" qualifiers="const">
@@ -547,7 +547,7 @@
<return type="int" />
<description>
Returns how many allocations the GPU driver has performed for internal driver structures.
- This is only used by Vulkan in Debug builds and can return 0 when this information is not tracked or unknown.
+ This is only used by Vulkan in debug builds and can return 0 when this information is not tracked or unknown.
</description>
</method>
<method name="get_driver_allocs_by_object_type" qualifiers="const">
@@ -556,7 +556,24 @@
<description>
Same as [method get_driver_allocation_count] but filtered for a given object type.
The type argument must be in range [code][0; get_tracked_object_type_count - 1][/code]. If [method get_tracked_object_type_count] is 0, then type argument is ignored and always returns 0.
- This is only used by Vulkan in Debug builds and can return 0 when this information is not tracked or unknown.
+ This is only used by Vulkan in debug builds and can return 0 when this information is not tracked or unknown.
+ </description>
+ </method>
+ <method name="get_driver_and_device_memory_report" qualifiers="const">
+ <return type="String" />
+ <description>
+ Returns string report in CSV format using the following methods:
+ - [method get_tracked_object_name]
+ - [method get_tracked_object_type_count]
+ - [method get_driver_total_memory]
+ - [method get_driver_allocation_count]
+ - [method get_driver_memory_by_object_type]
+ - [method get_driver_allocs_by_object_type]
+ - [method get_device_total_memory]
+ - [method get_device_allocation_count]
+ - [method get_device_memory_by_object_type]
+ - [method get_device_allocs_by_object_type]
+ This is only used by Vulkan in debug builds. Godot must also be started with the [code]--extra-gpu-memory-tracking[/code] [url=$DOCS_URL/tutorials/editor/command_line_tutorial.html]command line argument[/url].
</description>
</method>
<method name="get_driver_memory_by_object_type" qualifiers="const">
@@ -565,7 +582,7 @@
<description>
Same as [method get_driver_total_memory] but filtered for a given object type.
The type argument must be in range [code][0; get_tracked_object_type_count - 1][/code]. If [method get_tracked_object_type_count] is 0, then type argument is ignored and always returns 0.
- This is only used by Vulkan in Debug builds and can return 0 when this information is not tracked or unknown.
+ This is only used by Vulkan in debug builds and can return 0 when this information is not tracked or unknown.
</description>
</method>
<method name="get_driver_resource">
@@ -581,7 +598,7 @@
<return type="int" />
<description>
Returns how much bytes the GPU driver is using for internal driver structures.
- This is only used by Vulkan in Debug builds and can return 0 when this information is not tracked or unknown.
+ This is only used by Vulkan in debug builds and can return 0 when this information is not tracked or unknown.
</description>
</method>
<method name="get_frame_delay" qualifiers="const">
@@ -614,14 +631,14 @@
- SWAPCHAIN_KHR
- COMMAND_POOL
Thus if e.g. [code]get_tracked_object_name(5)[/code] returns "COMMAND_POOL", then [code]get_device_memory_by_object_type(5)[/code] returns the bytes used by the GPU for command pools.
- This is only used by Vulkan in Debug builds.
+ This is only used by Vulkan in debug builds. Godot must also be started with the [code]--extra-gpu-memory-tracking[/code] [url=$DOCS_URL/tutorials/editor/command_line_tutorial.html]command line argument[/url].
</description>
</method>
<method name="get_tracked_object_type_count" qualifiers="const">
<return type="int" />
<description>
Returns how many types of trackable objects are.
- This is only used by Vulkan in Debug builds.
+ This is only used by Vulkan in debug builds. Godot must also be started with the [code]--extra-gpu-memory-tracking[/code] [url=$DOCS_URL/tutorials/editor/command_line_tutorial.html]command line argument[/url].
</description>
</method>
<method name="index_array_create">
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/RichTextLabel.xml b/doc/classes/RichTextLabel.xml
index 7e0c39ac7c..9a772835a6 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].
diff --git a/doc/classes/ScriptEditor.xml b/doc/classes/ScriptEditor.xml
index 43ee4dda60..50adecccf5 100644
--- a/doc/classes/ScriptEditor.xml
+++ b/doc/classes/ScriptEditor.xml
@@ -40,7 +40,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/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..2fe2a0eaa1 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,7 +22,6 @@
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.
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/String.xml b/doc/classes/String.xml
index 450e483f69..de3d3e7cb9 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]
@@ -527,7 +526,6 @@
<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 +645,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 +799,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 +878,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..836ca6b4ba 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]
@@ -496,7 +495,6 @@
<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 +701,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 +780,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/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/drivers/d3d12/SCsub b/drivers/d3d12/SCsub
index 35227ebe08..482a549189 100644
--- a/drivers/d3d12/SCsub
+++ b/drivers/d3d12/SCsub
@@ -136,7 +136,6 @@ if env.msvc:
]
else:
extra_defines += [
- ("__REQUIRED_RPCNDR_H_VERSION__", 475),
"HAVE_STRUCT_TIMESPEC",
]
diff --git a/drivers/d3d12/rendering_device_driver_d3d12.h b/drivers/d3d12/rendering_device_driver_d3d12.h
index ac0ad41294..d8381279ec 100644
--- a/drivers/d3d12/rendering_device_driver_d3d12.h
+++ b/drivers/d3d12/rendering_device_driver_d3d12.h
@@ -36,6 +36,11 @@
#include "core/templates/self_list.h"
#include "servers/rendering/rendering_device_driver.h"
+#ifndef _MSC_VER
+// Match current version used by MinGW, MSVC and Direct3D 12 headers use 500.
+#define __REQUIRED_RPCNDR_H_VERSION__ 475
+#endif
+
#if defined(__GNUC__) && !defined(__clang__)
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wnon-virtual-dtor"
diff --git a/drivers/egl/egl_manager.cpp b/drivers/egl/egl_manager.cpp
index 9c1d08331d..4477ba7752 100644
--- a/drivers/egl/egl_manager.cpp
+++ b/drivers/egl/egl_manager.cpp
@@ -357,7 +357,7 @@ Error EGLManager::initialize(void *p_native_display) {
// have to temporarily get a proper display and reload EGL once again to
// initialize everything else.
if (!gladLoaderLoadEGL(EGL_NO_DISPLAY)) {
- ERR_FAIL_V_MSG(ERR_UNAVAILABLE, "Can't load EGL.");
+ ERR_FAIL_V_MSG(ERR_UNAVAILABLE, "Can't load EGL dynamic library.");
}
EGLDisplay tmp_display = EGL_NO_DISPLAY;
@@ -387,7 +387,7 @@ Error EGLManager::initialize(void *p_native_display) {
int version = gladLoaderLoadEGL(tmp_display);
if (!version) {
eglTerminate(tmp_display);
- ERR_FAIL_V_MSG(ERR_UNAVAILABLE, "Can't load EGL.");
+ ERR_FAIL_V_MSG(ERR_UNAVAILABLE, "Can't load EGL dynamic library.");
}
int major = GLAD_VERSION_MAJOR(version);
diff --git a/drivers/gles3/shaders/scene.glsl b/drivers/gles3/shaders/scene.glsl
index 3e3b4d11f7..ce2db7fa85 100644
--- a/drivers/gles3/shaders/scene.glsl
+++ b/drivers/gles3/shaders/scene.glsl
@@ -1813,16 +1813,10 @@ void main() {
vec3 n = normalize(lightmap_normal_xform * normal);
- ambient_light += lm_light_l0 * 0.282095f;
- ambient_light += lm_light_l1n1 * 0.32573 * n.y * lightmap_exposure_normalization;
- ambient_light += lm_light_l1_0 * 0.32573 * n.z * lightmap_exposure_normalization;
- ambient_light += lm_light_l1p1 * 0.32573 * n.x * lightmap_exposure_normalization;
- if (metallic > 0.01) { // Since the more direct bounced light is lost, we can kind of fake it with this trick.
- vec3 r = reflect(normalize(-vertex), normal);
- specular_light += lm_light_l1n1 * 0.32573 * r.y * lightmap_exposure_normalization;
- specular_light += lm_light_l1_0 * 0.32573 * r.z * lightmap_exposure_normalization;
- specular_light += lm_light_l1p1 * 0.32573 * r.x * lightmap_exposure_normalization;
- }
+ ambient_light += lm_light_l0 * lightmap_exposure_normalization;
+ ambient_light += lm_light_l1n1 * n.y * lightmap_exposure_normalization;
+ ambient_light += lm_light_l1_0 * n.z * lightmap_exposure_normalization;
+ ambient_light += lm_light_l1p1 * n.x * lightmap_exposure_normalization;
#else
#ifdef LIGHTMAP_BICUBIC_FILTER
ambient_light += textureArray_bicubic(lightmap_textures, uvw, lightmap_texture_size).rgb * lightmap_exposure_normalization;
diff --git a/drivers/gles3/storage/texture_storage.cpp b/drivers/gles3/storage/texture_storage.cpp
index 8251c8f52e..57fe96fb6f 100644
--- a/drivers/gles3/storage/texture_storage.cpp
+++ b/drivers/gles3/storage/texture_storage.cpp
@@ -1497,11 +1497,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]);
}
diff --git a/drivers/metal/metal_objects.h b/drivers/metal/metal_objects.h
index 70f86f2fac..f0a3e85f88 100644
--- a/drivers/metal/metal_objects.h
+++ b/drivers/metal/metal_objects.h
@@ -57,10 +57,12 @@
#import "servers/rendering/rendering_device_driver.h"
+#import <CommonCrypto/CommonDigest.h>
#import <Foundation/Foundation.h>
#import <Metal/Metal.h>
#import <QuartzCore/CAMetalLayer.h>
#import <simd/simd.h>
+#import <zlib.h>
#import <initializer_list>
#import <optional>
#import <spirv.hpp>
@@ -497,6 +499,76 @@ struct API_AVAILABLE(macos(11.0), ios(14.0)) UniformSet {
HashMap<RDC::ShaderStage, id<MTLArgumentEncoder>> encoders;
};
+struct ShaderCacheEntry;
+
+enum class ShaderLoadStrategy {
+ DEFAULT,
+ LAZY,
+};
+
+/// A Metal shader library.
+@interface MDLibrary : NSObject {
+ ShaderCacheEntry *_entry;
+};
+- (id<MTLLibrary>)library;
+- (NSError *)error;
+- (void)setLabel:(NSString *)label;
+
++ (instancetype)newLibraryWithCacheEntry:(ShaderCacheEntry *)entry
+ device:(id<MTLDevice>)device
+ source:(NSString *)source
+ options:(MTLCompileOptions *)options
+ strategy:(ShaderLoadStrategy)strategy;
+@end
+
+struct SHA256Digest {
+ unsigned char data[CC_SHA256_DIGEST_LENGTH];
+
+ uint32_t hash() const {
+ uint32_t c = crc32(0, data, CC_SHA256_DIGEST_LENGTH);
+ return c;
+ }
+
+ SHA256Digest() {
+ bzero(data, CC_SHA256_DIGEST_LENGTH);
+ }
+
+ 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 <>
+struct HashMapComparatorDefault<SHA256Digest> {
+ static bool compare(const SHA256Digest &p_lhs, const SHA256Digest &p_rhs) {
+ return memcmp(p_lhs.data, p_rhs.data, CC_SHA256_DIGEST_LENGTH) == 0;
+ }
+};
+
+/// A cache entry for a Metal shader library.
+struct ShaderCacheEntry {
+ RenderingDeviceDriverMetal &owner;
+ /// A hash of the Metal shader source code.
+ SHA256Digest key;
+ CharString name;
+ 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.
+ MDLibrary *__weak library = nil;
+
+ /// Notify the cache that this entry is no longer needed.
+ void notify_free() const;
+
+ ShaderCacheEntry(RenderingDeviceDriverMetal &p_owner, SHA256Digest p_key) :
+ owner(p_owner), key(p_key) {
+ }
+ ~ShaderCacheEntry() = default;
+};
+
class API_AVAILABLE(macos(11.0), ios(14.0)) MDShader {
public:
CharString name;
@@ -517,15 +589,14 @@ public:
} push_constants;
MTLSize local = {};
- id<MTLLibrary> kernel;
+ MDLibrary *kernel;
#if DEV_ENABLED
CharString kernel_source;
#endif
void encode_push_constant_data(VectorView<uint32_t> p_data, MDCommandBuffer *p_cb) final;
- MDComputeShader(CharString p_name, Vector<UniformSet> p_sets, id<MTLLibrary> p_kernel);
- ~MDComputeShader() override = default;
+ MDComputeShader(CharString p_name, Vector<UniformSet> p_sets, MDLibrary *p_kernel);
};
class API_AVAILABLE(macos(11.0), ios(14.0)) MDRenderShader final : public MDShader {
@@ -541,8 +612,8 @@ public:
} frag;
} push_constants;
- id<MTLLibrary> vert;
- id<MTLLibrary> frag;
+ MDLibrary *vert;
+ MDLibrary *frag;
#if DEV_ENABLED
CharString vert_source;
CharString frag_source;
@@ -550,8 +621,7 @@ public:
void encode_push_constant_data(VectorView<uint32_t> p_data, MDCommandBuffer *p_cb) final;
- MDRenderShader(CharString p_name, Vector<UniformSet> p_sets, id<MTLLibrary> p_vert, id<MTLLibrary> p_frag);
- ~MDRenderShader() override = default;
+ MDRenderShader(CharString p_name, Vector<UniformSet> p_sets, MDLibrary *p_vert, MDLibrary *p_frag);
};
enum StageResourceUsage : uint32_t {
diff --git a/drivers/metal/metal_objects.mm b/drivers/metal/metal_objects.mm
index 3ce00f74a3..c66b683e23 100644
--- a/drivers/metal/metal_objects.mm
+++ b/drivers/metal/metal_objects.mm
@@ -50,9 +50,12 @@
#import "metal_objects.h"
+#import "metal_utils.h"
#import "pixel_formats.h"
#import "rendering_device_driver_metal.h"
+#import <os/signpost.h>
+
void MDCommandBuffer::begin() {
DEV_ASSERT(commandBuffer == nil);
commandBuffer = queue.commandBuffer;
@@ -850,7 +853,7 @@ void MDCommandBuffer::_end_blit() {
type = MDCommandBufferStateType::None;
}
-MDComputeShader::MDComputeShader(CharString p_name, Vector<UniformSet> p_sets, id<MTLLibrary> p_kernel) :
+MDComputeShader::MDComputeShader(CharString p_name, Vector<UniformSet> p_sets, MDLibrary *p_kernel) :
MDShader(p_name, p_sets), kernel(p_kernel) {
}
@@ -868,7 +871,7 @@ void MDComputeShader::encode_push_constant_data(VectorView<uint32_t> p_data, MDC
[enc setBytes:ptr length:length atIndex:push_constants.binding];
}
-MDRenderShader::MDRenderShader(CharString p_name, Vector<UniformSet> p_sets, id<MTLLibrary> _Nonnull p_vert, id<MTLLibrary> _Nonnull p_frag) :
+MDRenderShader::MDRenderShader(CharString p_name, Vector<UniformSet> p_sets, MDLibrary *_Nonnull p_vert, MDLibrary *_Nonnull p_frag) :
MDShader(p_name, p_sets), vert(p_vert), frag(p_frag) {
}
@@ -1378,3 +1381,197 @@ id<MTLDepthStencilState> MDResourceCache::get_depth_stencil_state(bool p_use_dep
}
return *val;
}
+
+static const char *SHADER_STAGE_NAMES[] = {
+ [RD::SHADER_STAGE_VERTEX] = "vert",
+ [RD::SHADER_STAGE_FRAGMENT] = "frag",
+ [RD::SHADER_STAGE_TESSELATION_CONTROL] = "tess_ctrl",
+ [RD::SHADER_STAGE_TESSELATION_EVALUATION] = "tess_eval",
+ [RD::SHADER_STAGE_COMPUTE] = "comp",
+};
+
+void ShaderCacheEntry::notify_free() const {
+ owner.shader_cache_free_entry(key);
+}
+
+@interface MDLibrary ()
+- (instancetype)initWithCacheEntry:(ShaderCacheEntry *)entry;
+@end
+
+/// Loads the MTLLibrary when the library is first accessed.
+@interface MDLazyLibrary : MDLibrary {
+ id<MTLLibrary> _library;
+ NSError *_error;
+ std::shared_mutex _mu;
+ bool _loaded;
+ id<MTLDevice> _device;
+ NSString *_source;
+ MTLCompileOptions *_options;
+}
+- (instancetype)initWithCacheEntry:(ShaderCacheEntry *)entry
+ device:(id<MTLDevice>)device
+ source:(NSString *)source
+ options:(MTLCompileOptions *)options;
+@end
+
+/// Loads the MTLLibrary immediately on initialization, using an asynchronous API.
+@interface MDImmediateLibrary : MDLibrary {
+ id<MTLLibrary> _library;
+ NSError *_error;
+ std::mutex _cv_mutex;
+ std::condition_variable _cv;
+ std::atomic<bool> _complete;
+ bool _ready;
+}
+- (instancetype)initWithCacheEntry:(ShaderCacheEntry *)entry
+ device:(id<MTLDevice>)device
+ source:(NSString *)source
+ options:(MTLCompileOptions *)options;
+@end
+
+@implementation MDLibrary
+
++ (instancetype)newLibraryWithCacheEntry:(ShaderCacheEntry *)entry
+ device:(id<MTLDevice>)device
+ source:(NSString *)source
+ options:(MTLCompileOptions *)options
+ strategy:(ShaderLoadStrategy)strategy {
+ switch (strategy) {
+ case ShaderLoadStrategy::DEFAULT:
+ [[fallthrough]];
+ default:
+ return [[MDImmediateLibrary alloc] initWithCacheEntry:entry device:device source:source options:options];
+ case ShaderLoadStrategy::LAZY:
+ return [[MDLazyLibrary alloc] initWithCacheEntry:entry device:device source:source options:options];
+ }
+}
+
+- (id<MTLLibrary>)library {
+ CRASH_NOW_MSG("Not implemented");
+ return nil;
+}
+
+- (NSError *)error {
+ CRASH_NOW_MSG("Not implemented");
+ return nil;
+}
+
+- (void)setLabel:(NSString *)label {
+}
+
+- (instancetype)initWithCacheEntry:(ShaderCacheEntry *)entry {
+ self = [super init];
+ _entry = entry;
+ _entry->library = self;
+ return self;
+}
+
+- (void)dealloc {
+ _entry->notify_free();
+}
+
+@end
+
+@implementation MDImmediateLibrary
+
+- (instancetype)initWithCacheEntry:(ShaderCacheEntry *)entry
+ device:(id<MTLDevice>)device
+ source:(NSString *)source
+ options:(MTLCompileOptions *)options {
+ self = [super initWithCacheEntry:entry];
+ _complete = false;
+ _ready = false;
+
+ __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=%X",
+ entry->name.get_data(), SHADER_STAGE_NAMES[entry->stage], entry->key.short_sha());
+
+ [device newLibraryWithSource:source
+ options:options
+ completionHandler:^(id<MTLLibrary> library, NSError *error) {
+ os_signpost_interval_end(LOG_INTERVALS, compile_id, "shader_compile");
+ self->_library = library;
+ self->_error = error;
+ if (error) {
+ ERR_PRINT(String(U"Error compiling shader %s: %s").format(entry->name.get_data(), error.localizedDescription.UTF8String));
+ }
+
+ {
+ std::lock_guard<std::mutex> lock(self->_cv_mutex);
+ _ready = true;
+ }
+ _cv.notify_all();
+ _complete = true;
+ }];
+ return self;
+}
+
+- (id<MTLLibrary>)library {
+ if (!_complete) {
+ std::unique_lock<std::mutex> lock(_cv_mutex);
+ _cv.wait(lock, [&] { return _ready; });
+ }
+ return _library;
+}
+
+- (NSError *)error {
+ if (!_complete) {
+ std::unique_lock<std::mutex> lock(_cv_mutex);
+ _cv.wait(lock, [&] { return _ready; });
+ }
+ return _error;
+}
+
+@end
+
+@implementation MDLazyLibrary
+- (instancetype)initWithCacheEntry:(ShaderCacheEntry *)entry
+ device:(id<MTLDevice>)device
+ source:(NSString *)source
+ options:(MTLCompileOptions *)options {
+ self = [super initWithCacheEntry:entry];
+ _device = device;
+ _source = source;
+ _options = options;
+
+ return self;
+}
+
+- (void)load {
+ {
+ std::shared_lock<std::shared_mutex> lock(_mu);
+ if (_loaded) {
+ return;
+ }
+ }
+
+ std::unique_lock<std::shared_mutex> lock(_mu);
+ if (_loaded) {
+ return;
+ }
+
+ __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=%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");
+ _device = nil;
+ _source = nil;
+ _options = nil;
+ _loaded = true;
+}
+
+- (id<MTLLibrary>)library {
+ [self load];
+ return _library;
+}
+
+- (NSError *)error {
+ [self load];
+ return _error;
+}
+
+@end
diff --git a/drivers/metal/metal_utils.h b/drivers/metal/metal_utils.h
index eed1aad89b..f3ee395d04 100644
--- a/drivers/metal/metal_utils.h
+++ b/drivers/metal/metal_utils.h
@@ -31,6 +31,8 @@
#ifndef METAL_UTILS_H
#define METAL_UTILS_H
+#import <os/log.h>
+
#pragma mark - Boolean flags
namespace flags {
@@ -78,4 +80,22 @@ static constexpr uint64_t round_up_to_alignment(uint64_t p_value, uint64_t p_ali
return aligned_value;
}
+class Defer {
+public:
+ Defer(std::function<void()> func) :
+ func_(func) {}
+ ~Defer() { func_(); }
+
+private:
+ std::function<void()> func_;
+};
+
+#define CONCAT_INTERNAL(x, y) x##y
+#define CONCAT(x, y) CONCAT_INTERNAL(x, y)
+#define DEFER const Defer &CONCAT(defer__, __LINE__) = Defer
+
+extern os_log_t LOG_DRIVER;
+// Used for dynamic tracing.
+extern os_log_t LOG_INTERVALS;
+
#endif // METAL_UTILS_H
diff --git a/drivers/metal/rendering_device_driver_metal.h b/drivers/metal/rendering_device_driver_metal.h
index 1bb71583ab..7c23624e43 100644
--- a/drivers/metal/rendering_device_driver_metal.h
+++ b/drivers/metal/rendering_device_driver_metal.h
@@ -48,6 +48,8 @@
class RenderingContextDriverMetal;
class API_AVAILABLE(macos(11.0), ios(14.0)) RenderingDeviceDriverMetal : public RenderingDeviceDriver {
+ friend struct ShaderCacheEntry;
+
template <typename T>
using Result = std::variant<T, Error>;
@@ -77,6 +79,19 @@ class API_AVAILABLE(macos(11.0), ios(14.0)) RenderingDeviceDriverMetal : public
Error _create_device();
Error _check_capabilities();
+#pragma mark - Shader Cache
+
+ ShaderLoadStrategy _shader_load_strategy = ShaderLoadStrategy::DEFAULT;
+
+ /**
+ * The shader cache is a map of hashes of the Metal source to shader cache entries.
+ *
+ * To prevent unbounded growth of the cache, cache entries are automatically freed when
+ * there are no more references to the MDLibrary associated with the cache entry.
+ */
+ HashMap<SHA256Digest, ShaderCacheEntry *, HashableHasher<SHA256Digest>> _shader_cache;
+ void shader_cache_free_entry(const SHA256Digest &key);
+
public:
Error initialize(uint32_t p_device_index, uint32_t p_frame_count) override final;
@@ -270,7 +285,7 @@ public:
#pragma mark Pipeline
private:
- Result<id<MTLFunction>> _create_function(id<MTLLibrary> p_library, NSString *p_name, VectorView<PipelineSpecializationConstant> &p_specialization_constants);
+ Result<id<MTLFunction>> _create_function(MDLibrary *p_library, NSString *p_name, VectorView<PipelineSpecializationConstant> &p_specialization_constants);
public:
virtual void pipeline_free(PipelineID p_pipeline_id) override final;
diff --git a/drivers/metal/rendering_device_driver_metal.mm b/drivers/metal/rendering_device_driver_metal.mm
index 0a9fde06a7..0d88a14fbc 100644
--- a/drivers/metal/rendering_device_driver_metal.mm
+++ b/drivers/metal/rendering_device_driver_metal.mm
@@ -60,9 +60,22 @@
#import <Metal/MTLTexture.h>
#import <Metal/Metal.h>
+#import <os/log.h>
+#import <os/signpost.h>
#import <spirv_msl.hpp>
#import <spirv_parser.hpp>
+#pragma mark - Logging
+
+os_log_t LOG_DRIVER;
+// Used for dynamic tracing.
+os_log_t LOG_INTERVALS;
+
+__attribute__((constructor)) static void InitializeLogging(void) {
+ 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");
+}
+
/*****************/
/**** GENERIC ****/
/*****************/
@@ -2258,6 +2271,15 @@ Vector<uint8_t> RenderingDeviceDriverMetal::shader_compile_binary_from_spirv(Vec
return ret;
}
+void RenderingDeviceDriverMetal::shader_cache_free_entry(const SHA256Digest &key) {
+ if (ShaderCacheEntry **pentry = _shader_cache.getptr(key); pentry != nullptr) {
+ ShaderCacheEntry *entry = *pentry;
+ _shader_cache.erase(key);
+ entry->library = nil;
+ memdelete(entry);
+ }
+}
+
RDD::ShaderID RenderingDeviceDriverMetal::shader_create_from_bytecode(const Vector<uint8_t> &p_shader_binary, ShaderDescription &r_shader_desc, String &r_name) {
r_shader_desc = {}; // Driver-agnostic.
@@ -2285,18 +2307,30 @@ RDD::ShaderID RenderingDeviceDriverMetal::shader_create_from_bytecode(const Vect
MTLCompileOptions *options = [MTLCompileOptions new];
options.languageVersion = binary_data.get_msl_version();
- HashMap<ShaderStage, id<MTLLibrary>> libraries;
+ HashMap<ShaderStage, MDLibrary *> libraries;
+
for (ShaderStageData &shader_data : binary_data.stages) {
- NSString *source = [[NSString alloc] initWithBytesNoCopy:(void *)shader_data.source.ptr()
- length:shader_data.source.length()
- encoding:NSUTF8StringEncoding
- freeWhenDone:NO];
- NSError *error = nil;
- id<MTLLibrary> library = [device newLibraryWithSource:source options:options error:&error];
- if (error != nil) {
- print_error(error.localizedDescription.UTF8String);
- ERR_FAIL_V_MSG(ShaderID(), "failed to compile Metal source");
+ SHA256Digest key = SHA256Digest(shader_data.source.ptr(), shader_data.source.length());
+
+ if (ShaderCacheEntry **p = _shader_cache.getptr(key); p != nullptr) {
+ libraries[shader_data.stage] = (*p)->library;
+ continue;
}
+
+ NSString *source = [[NSString alloc] initWithBytes:(void *)shader_data.source.ptr()
+ length:shader_data.source.length()
+ encoding:NSUTF8StringEncoding];
+
+ ShaderCacheEntry *cd = memnew(ShaderCacheEntry(*this, key));
+ cd->name = binary_data.shader_name;
+ cd->stage = shader_data.stage;
+
+ MDLibrary *library = [MDLibrary newLibraryWithCacheEntry:cd
+ device:device
+ source:source
+ options:options
+ strategy:_shader_load_strategy];
+ _shader_cache[key] = cd;
libraries[shader_data.stage] = library;
}
@@ -3062,8 +3096,13 @@ void RenderingDeviceDriverMetal::command_render_set_line_width(CommandBufferID p
// ----- PIPELINE -----
-RenderingDeviceDriverMetal::Result<id<MTLFunction>> RenderingDeviceDriverMetal::_create_function(id<MTLLibrary> p_library, NSString *p_name, VectorView<PipelineSpecializationConstant> &p_specialization_constants) {
- id<MTLFunction> function = [p_library newFunctionWithName:p_name];
+RenderingDeviceDriverMetal::Result<id<MTLFunction>> RenderingDeviceDriverMetal::_create_function(MDLibrary *p_library, NSString *p_name, VectorView<PipelineSpecializationConstant> &p_specialization_constants) {
+ id<MTLLibrary> library = p_library.library;
+ if (!library) {
+ ERR_FAIL_V_MSG(ERR_CANT_CREATE, "Failed to compile Metal library");
+ }
+
+ id<MTLFunction> function = [library newFunctionWithName:p_name];
ERR_FAIL_NULL_V_MSG(function, ERR_CANT_CREATE, "No function named main0");
if (function.functionConstantsDictionary.count == 0) {
@@ -3091,12 +3130,22 @@ RenderingDeviceDriverMetal::Result<id<MTLFunction>> RenderingDeviceDriverMetal::
}];
}
+ // Initialize an array of integers representing the indexes of p_specialization_constants
+ uint32_t *indexes = (uint32_t *)alloca(p_specialization_constants.size() * sizeof(uint32_t));
+ for (uint32_t i = 0; i < p_specialization_constants.size(); i++) {
+ indexes[i] = i;
+ }
+ // Sort the array of integers based on the values in p_specialization_constants
+ std::sort(indexes, &indexes[p_specialization_constants.size()], [&](int a, int b) {
+ return p_specialization_constants[a].constant_id < p_specialization_constants[b].constant_id;
+ });
+
MTLFunctionConstantValues *constantValues = [MTLFunctionConstantValues new];
uint32_t i = 0;
uint32_t j = 0;
while (i < constants.count && j < p_specialization_constants.size()) {
MTLFunctionConstant *curr = constants[i];
- PipelineSpecializationConstant const &sc = p_specialization_constants[j];
+ PipelineSpecializationConstant const &sc = p_specialization_constants[indexes[j]];
if (curr.index == sc.constant_id) {
switch (curr.type) {
case MTLDataTypeBool:
@@ -3131,9 +3180,9 @@ RenderingDeviceDriverMetal::Result<id<MTLFunction>> RenderingDeviceDriverMetal::
}
NSError *err = nil;
- function = [p_library newFunctionWithName:@"main0"
- constantValues:constantValues
- error:&err];
+ function = [library newFunctionWithName:@"main0"
+ constantValues:constantValues
+ error:&err];
ERR_FAIL_NULL_V_MSG(function, ERR_CANT_CREATE, String("specialized function failed: ") + err.localizedDescription.UTF8String);
return function;
@@ -3178,6 +3227,14 @@ RDD::PipelineID RenderingDeviceDriverMetal::render_pipeline_create(
MTLVertexDescriptor *vert_desc = rid::get(p_vertex_format);
MDRenderPass *pass = (MDRenderPass *)(p_render_pass.id);
+ os_signpost_id_t reflect_id = os_signpost_id_make_with_pointer(LOG_INTERVALS, shader);
+ os_signpost_interval_begin(LOG_INTERVALS, reflect_id, "render_pipeline_create", "shader_name=%{public}s", shader->name.get_data());
+ DEFER([=]() {
+ os_signpost_interval_end(LOG_INTERVALS, reflect_id, "render_pipeline_create");
+ });
+
+ os_signpost_event_emit(LOG_DRIVER, OS_SIGNPOST_ID_EXCLUSIVE, "create_pipeline");
+
MTLRenderPipelineDescriptor *desc = [MTLRenderPipelineDescriptor new];
{
@@ -3472,9 +3529,15 @@ void RenderingDeviceDriverMetal::command_compute_dispatch_indirect(CommandBuffer
RDD::PipelineID RenderingDeviceDriverMetal::compute_pipeline_create(ShaderID p_shader, VectorView<PipelineSpecializationConstant> p_specialization_constants) {
MDComputeShader *shader = (MDComputeShader *)(p_shader.id);
- id<MTLLibrary> library = shader->kernel;
+ os_signpost_id_t reflect_id = os_signpost_id_make_with_pointer(LOG_INTERVALS, shader);
+ os_signpost_interval_begin(LOG_INTERVALS, reflect_id, "compute_pipeline_create", "shader_name=%{public}s", shader->name.get_data());
+ DEFER([=]() {
+ os_signpost_interval_end(LOG_INTERVALS, reflect_id, "compute_pipeline_create");
+ });
+
+ os_signpost_event_emit(LOG_DRIVER, OS_SIGNPOST_ID_EXCLUSIVE, "create_pipeline");
- Result<id<MTLFunction>> function_or_err = _create_function(library, @"main0", p_specialization_constants);
+ Result<id<MTLFunction>> function_or_err = _create_function(shader->kernel, @"main0", p_specialization_constants);
ERR_FAIL_COND_V(std::holds_alternative<Error>(function_or_err), PipelineID());
id<MTLFunction> function = std::get<id<MTLFunction>>(function_or_err);
@@ -3575,12 +3638,13 @@ void RenderingDeviceDriverMetal::set_object_name(ObjectType p_type, ID p_driver_
buffer.label = [NSString stringWithUTF8String:p_name.utf8().get_data()];
} break;
case OBJECT_TYPE_SHADER: {
+ NSString *label = [NSString stringWithUTF8String:p_name.utf8().get_data()];
MDShader *shader = (MDShader *)(p_driver_id.id);
if (MDRenderShader *rs = dynamic_cast<MDRenderShader *>(shader); rs != nullptr) {
- rs->vert.label = [NSString stringWithUTF8String:p_name.utf8().get_data()];
- rs->frag.label = [NSString stringWithUTF8String:p_name.utf8().get_data()];
+ [rs->vert setLabel:label];
+ [rs->frag setLabel:label];
} else if (MDComputeShader *cs = dynamic_cast<MDComputeShader *>(shader); cs != nullptr) {
- cs->kernel.label = [NSString stringWithUTF8String:p_name.utf8().get_data()];
+ [cs->kernel setLabel:label];
} else {
DEV_ASSERT(false);
}
@@ -3769,7 +3833,7 @@ uint64_t RenderingDeviceDriverMetal::api_trait_get(ApiTrait p_trait) {
bool RenderingDeviceDriverMetal::has_feature(Features p_feature) {
switch (p_feature) {
case SUPPORTS_MULTIVIEW:
- return true;
+ return false;
case SUPPORTS_FSR_HALF_FLOAT:
return true;
case SUPPORTS_ATTACHMENT_VRS:
@@ -3820,12 +3884,20 @@ size_t RenderingDeviceDriverMetal::get_texel_buffer_alignment_for_format(MTLPixe
RenderingDeviceDriverMetal::RenderingDeviceDriverMetal(RenderingContextDriverMetal *p_context_driver) :
context_driver(p_context_driver) {
DEV_ASSERT(p_context_driver != nullptr);
+
+ if (String res = OS::get_singleton()->get_environment("GODOT_MTL_SHADER_LOAD_STRATEGY"); res == U"lazy") {
+ _shader_load_strategy = ShaderLoadStrategy::LAZY;
+ }
}
RenderingDeviceDriverMetal::~RenderingDeviceDriverMetal() {
for (MDCommandBuffer *cb : command_buffers) {
delete cb;
}
+
+ for (KeyValue<SHA256Digest, ShaderCacheEntry *> &kv : _shader_cache) {
+ memdelete(kv.value);
+ }
}
#pragma mark - Initialization
diff --git a/drivers/vulkan/rendering_context_driver_vulkan.cpp b/drivers/vulkan/rendering_context_driver_vulkan.cpp
index 6ffbb91516..df9bd98624 100644
--- a/drivers/vulkan/rendering_context_driver_vulkan.cpp
+++ b/drivers/vulkan/rendering_context_driver_vulkan.cpp
@@ -106,7 +106,7 @@ const char *RenderingContextDriverVulkan::get_tracked_object_name(uint32_t p_typ
return vkTrackedObjectTypeNames[p_type_index];
#else
- return "VK_TRACK_DRIVER_* disabled at build time";
+ return "VK_TRACK_*_MEMORY disabled at build time";
#endif
}
@@ -120,6 +120,8 @@ uint64_t RenderingContextDriverVulkan::get_tracked_object_type_count() const {
RenderingContextDriverVulkan::VkTrackedObjectType vk_object_to_tracked_object(VkObjectType p_type) {
if (p_type > VK_OBJECT_TYPE_COMMAND_POOL && p_type != (VkObjectType)RenderingContextDriverVulkan::VK_TRACKED_OBJECT_TYPE_VMA) {
switch (p_type) {
+ case VK_OBJECT_TYPE_DESCRIPTOR_UPDATE_TEMPLATE:
+ return RenderingContextDriverVulkan::VK_TRACKED_OBJECT_DESCRIPTOR_UPDATE_TEMPLATE_KHR;
case VK_OBJECT_TYPE_SURFACE_KHR:
return RenderingContextDriverVulkan::VK_TRACKED_OBJECT_TYPE_SURFACE;
case VK_OBJECT_TYPE_SWAPCHAIN_KHR:
@@ -128,6 +130,9 @@ RenderingContextDriverVulkan::VkTrackedObjectType vk_object_to_tracked_object(Vk
return RenderingContextDriverVulkan::VK_TRACKED_OBJECT_TYPE_DEBUG_UTILS_MESSENGER_EXT;
case VK_OBJECT_TYPE_DEBUG_REPORT_CALLBACK_EXT:
return RenderingContextDriverVulkan::VK_TRACKED_OBJECT_TYPE_DEBUG_REPORT_CALLBACK_EXT;
+ case VK_OBJECT_TYPE_ACCELERATION_STRUCTURE_KHR:
+ case VK_OBJECT_TYPE_ACCELERATION_STRUCTURE_NV:
+ return RenderingContextDriverVulkan::VK_TRACKED_OBJECT_TYPE_ACCELERATION_STRUCTURE;
default:
_err_print_error(FUNCTION_STR, __FILE__, __LINE__, "Unknown VkObjectType enum value " + itos((uint32_t)p_type) + ".Please add it to VkTrackedObjectType, switch statement in "
"vk_object_to_tracked_object and get_tracked_object_name.",
@@ -229,6 +234,16 @@ VkAllocationCallbacks *RenderingContextDriverVulkan::get_allocation_callbacks(Vk
#if !defined(VK_TRACK_DRIVER_MEMORY)
return nullptr;
#else
+ if (!Engine::get_singleton()->is_extra_gpu_memory_tracking_enabled()) {
+ return nullptr;
+ }
+
+#ifdef _MSC_VER
+#define LAMBDA_VK_CALL_CONV
+#else
+#define LAMBDA_VK_CALL_CONV VKAPI_PTR
+#endif
+
struct TrackedMemHeader {
size_t size;
VkSystemAllocationScope allocation_scope;
@@ -241,7 +256,7 @@ VkAllocationCallbacks *RenderingContextDriverVulkan::get_allocation_callbacks(Vk
void *p_user_data,
size_t size,
size_t alignment,
- VkSystemAllocationScope allocation_scope) -> void * {
+ VkSystemAllocationScope allocation_scope) LAMBDA_VK_CALL_CONV -> void * {
static constexpr size_t tracking_data_size = 32;
VkTrackedObjectType type = static_cast<VkTrackedObjectType>(*reinterpret_cast<VkTrackedObjectType *>(p_user_data));
@@ -274,7 +289,7 @@ VkAllocationCallbacks *RenderingContextDriverVulkan::get_allocation_callbacks(Vk
void *p_original,
size_t size,
size_t alignment,
- VkSystemAllocationScope allocation_scope) -> void * {
+ VkSystemAllocationScope allocation_scope) LAMBDA_VK_CALL_CONV -> void * {
if (p_original == nullptr) {
VkObjectType type = static_cast<VkObjectType>(*reinterpret_cast<uint32_t *>(p_user_data));
return get_allocation_callbacks(type)->pfnAllocation(p_user_data, size, alignment, allocation_scope);
@@ -305,7 +320,7 @@ VkAllocationCallbacks *RenderingContextDriverVulkan::get_allocation_callbacks(Vk
// Free function
[](
void *p_user_data,
- void *p_memory) {
+ void *p_memory) LAMBDA_VK_CALL_CONV {
if (!p_memory) {
return;
}
@@ -326,13 +341,13 @@ VkAllocationCallbacks *RenderingContextDriverVulkan::get_allocation_callbacks(Vk
void *p_user_data,
size_t size,
VkInternalAllocationType allocation_type,
- VkSystemAllocationScope allocation_scope) {
+ VkSystemAllocationScope allocation_scope) LAMBDA_VK_CALL_CONV {
},
[](
void *p_user_data,
size_t size,
VkInternalAllocationType allocation_type,
- VkSystemAllocationScope allocation_scope) {
+ VkSystemAllocationScope allocation_scope) LAMBDA_VK_CALL_CONV {
},
};
diff --git a/drivers/vulkan/rendering_context_driver_vulkan.h b/drivers/vulkan/rendering_context_driver_vulkan.h
index e70d17e131..4fbca012c6 100644
--- a/drivers/vulkan/rendering_context_driver_vulkan.h
+++ b/drivers/vulkan/rendering_context_driver_vulkan.h
@@ -170,10 +170,12 @@ public:
#if defined(VK_TRACK_DRIVER_MEMORY) || defined(VK_TRACK_DEVICE_MEMORY)
enum VkTrackedObjectType{
- VK_TRACKED_OBJECT_TYPE_SURFACE = VK_OBJECT_TYPE_COMMAND_POOL + 1,
+ VK_TRACKED_OBJECT_DESCRIPTOR_UPDATE_TEMPLATE_KHR = VK_OBJECT_TYPE_COMMAND_POOL + 1,
+ VK_TRACKED_OBJECT_TYPE_SURFACE,
VK_TRACKED_OBJECT_TYPE_SWAPCHAIN,
VK_TRACKED_OBJECT_TYPE_DEBUG_UTILS_MESSENGER_EXT,
VK_TRACKED_OBJECT_TYPE_DEBUG_REPORT_CALLBACK_EXT,
+ VK_TRACKED_OBJECT_TYPE_ACCELERATION_STRUCTURE,
VK_TRACKED_OBJECT_TYPE_VMA,
VK_TRACKED_OBJECT_TYPE_COUNT
};
diff --git a/drivers/vulkan/rendering_device_driver_vulkan.cpp b/drivers/vulkan/rendering_device_driver_vulkan.cpp
index 092af13b21..2ba353868b 100644
--- a/drivers/vulkan/rendering_device_driver_vulkan.cpp
+++ b/drivers/vulkan/rendering_device_driver_vulkan.cpp
@@ -503,7 +503,9 @@ Error RenderingDeviceDriverVulkan::_initialize_device_extensions() {
}
#if defined(VK_TRACK_DEVICE_MEMORY)
- _register_requested_device_extension(VK_EXT_DEVICE_MEMORY_REPORT_EXTENSION_NAME, false);
+ if (Engine::get_singleton()->is_extra_gpu_memory_tracking_enabled()) {
+ _register_requested_device_extension(VK_EXT_DEVICE_MEMORY_REPORT_EXTENSION_NAME, false);
+ }
#endif
_register_requested_device_extension(VK_EXT_DEVICE_FAULT_EXTENSION_NAME, false);
@@ -5044,6 +5046,8 @@ void RenderingDeviceDriverVulkan::on_device_lost() const {
if (fault_info.pAddressInfos) {
memfree(fault_info.pAddressInfos);
}
+
+ _err_print_error(FUNCTION_STR, __FILE__, __LINE__, context_driver->get_driver_and_device_memory_report());
}
void RenderingDeviceDriverVulkan::print_lost_device_info() {
diff --git a/editor/animation_track_editor.cpp b/editor/animation_track_editor.cpp
index 1f0ad13451..f4403780c2 100644
--- a/editor/animation_track_editor.cpp
+++ b/editor/animation_track_editor.cpp
@@ -1855,6 +1855,8 @@ void AnimationTimelineEdit::gui_input(const Ref<InputEvent> &p_event) {
if (dragging_hsize) {
int ofs = mm->get_position().x - dragging_hsize_from;
name_limit = dragging_hsize_at + ofs;
+ // Make sure name_limit is clamped to the range that UI allows.
+ name_limit = get_name_limit();
queue_redraw();
emit_signal(SNAME("name_limit_changed"));
play_position->queue_redraw();
@@ -4498,7 +4500,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: {
diff --git a/editor/editor_file_system.cpp b/editor/editor_file_system.cpp
index f75e438582..22ff7a4509 100644
--- a/editor/editor_file_system.cpp
+++ b/editor/editor_file_system.cpp
@@ -228,15 +228,18 @@ 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;
+ 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.
+ ep.step(TTR("Loading global class names..."), 1, true);
_first_scan_process_scripts(first_scan_root_dir, existing_class_names);
// Removing invalid global class to prevent having invalid paths in ScriptServer.
@@ -245,8 +248,13 @@ void EditorFileSystem::_first_scan_filesystem() {
// 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) {
@@ -650,6 +658,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 +795,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
@@ -872,8 +892,6 @@ void EditorFileSystem::scan() {
scan_total = 0;
s.priority = Thread::PRIORITY_LOW;
thread.start(_thread_func, this, s);
- //tree->hide();
- //progress->show();
}
}
@@ -1022,9 +1040,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;
@@ -1830,10 +1847,21 @@ void EditorFileSystem::_update_script_classes() {
update_script_mutex.lock();
+ 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();
@@ -1858,6 +1886,12 @@ void EditorFileSystem::_update_script_documentation() {
update_script_mutex.lock();
+ 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,8 +1914,14 @@ 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();
}
@@ -1916,7 +1956,7 @@ 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;
@@ -1938,7 +1978,7 @@ void EditorFileSystem::_update_scene_groups() {
}
if (ep) {
- ep->step(TTR("Updating Scene Groups..."), step_count++);
+ ep->step(efd->files[index]->file, step_count++);
}
}
@@ -2654,13 +2694,15 @@ 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()));
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 +2742,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 +2764,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 +2786,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 +2800,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 +2817,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);
@@ -2784,15 +2828,20 @@ void EditorFileSystem::reimport_files(const Vector<String> &p_files) {
}
ResourceUID::get_singleton()->update_cache(); // After reimporting, update the cache.
-
_save_filesystem_cache();
+
+ memdelete_notnull(ep);
+
_process_update_pending();
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) {
@@ -2955,7 +3004,10 @@ bool EditorFileSystem::_scan_extensions() {
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]);
+ // The extension may not have a .gdextension file.
+ if (!FileAccess::exists(loaded_extensions[i])) {
+ extensions_removed.push_back(loaded_extensions[i]);
+ }
}
}
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_log.cpp b/editor/editor_log.cpp
index 0dfbcd0e0d..baa44f56c4 100644
--- a/editor/editor_log.cpp
+++ b/editor/editor_log.cpp
@@ -398,7 +398,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);
}
}
diff --git a/editor/editor_node.cpp b/editor/editor_node.cpp
index a8f8edaae8..d19d4740ee 100644
--- a/editor/editor_node.cpp
+++ b/editor/editor_node.cpp
@@ -702,6 +702,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 +732,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: {
@@ -1073,30 +1076,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 +1115,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 +1243,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);
@@ -5214,6 +5230,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 +5256,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 +5279,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 +5350,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;
@@ -6328,8 +6387,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());
}
@@ -7319,9 +7376,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
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_settings.cpp b/editor/editor_settings.cpp
index b9d530353c..a4cb3fbb68 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, "")
diff --git a/editor/export/editor_export_platform.cpp b/editor/export/editor_export_platform.cpp
index 0768ae128b..8b31eda3bc 100644
--- a/editor/export/editor_export_platform.cpp
+++ b/editor/export/editor_export_platform.cpp
@@ -1861,7 +1861,6 @@ void EditorExportPlatform::gen_export_flags(Vector<String> &r_flags, int p_flags
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 +1885,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);
diff --git a/editor/export/export_template_manager.cpp b/editor/export/export_template_manager.cpp
index 0caf0ee066..9dfbe515d2 100644
--- a/editor/export/export_template_manager.cpp
+++ b/editor/export/export_template_manager.cpp
@@ -111,7 +111,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."));
}
}
@@ -921,11 +923,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"));
diff --git a/editor/export/export_template_manager.h b/editor/export/export_template_manager.h
index b1c5855878..5227e43a6e 100644
--- a/editor/export/export_template_manager.h
+++ b/editor/export/export_template_manager.h
@@ -58,7 +58,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;
diff --git a/editor/export/project_export.cpp b/editor/export/project_export.cpp
index 3103e504b9..351afa3810 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) {
@@ -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/gui/scene_tree_editor.cpp b/editor/gui/scene_tree_editor.cpp
index 87d8ddad09..69ab9810a0 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);
}
diff --git a/editor/import/3d/resource_importer_scene.cpp b/editor/import/3d/resource_importer_scene.cpp
index 8ad8e6201e..d0d7142c01 100644
--- a/editor/import/3d/resource_importer_scene.cpp
+++ b/editor/import/3d/resource_importer_scene.cpp
@@ -3058,9 +3058,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"];
+
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);
diff --git a/editor/import/3d/scene_import_settings.cpp b/editor/import/3d/scene_import_settings.cpp
index ed3eaa94c1..7cd5279b63 100644
--- a/editor/import/3d/scene_import_settings.cpp
+++ b/editor/import/3d/scene_import_settings.cpp
@@ -472,6 +472,7 @@ void SceneImportSettingsDialog::_fill_scene(Node *p_node, TreeItem *p_parent_ite
}
AABB aabb = accum_xform.xform(mesh_node->get_mesh()->get_aabb());
+
if (first_aabb) {
contents_aabb = aabb;
first_aabb = false;
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/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/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/mesh_instance_3d_editor_plugin.cpp b/editor/plugins/mesh_instance_3d_editor_plugin.cpp
index 4ebacbd0b3..369d6ab009 100644
--- a/editor/plugins/mesh_instance_3d_editor_plugin.cpp
+++ b/editor/plugins/mesh_instance_3d_editor_plugin.cpp
@@ -214,28 +214,7 @@ void MeshInstance3DEditor::_menu_option(int p_option) {
} break;
case MENU_OPTION_CREATE_NAVMESH: {
- Ref<NavigationMesh> nmesh = memnew(NavigationMesh);
-
- if (nmesh.is_null()) {
- return;
- }
-
- nmesh->create_from_mesh(mesh);
- NavigationRegion3D *nmi = memnew(NavigationRegion3D);
- nmi->set_navigation_mesh(nmesh);
-
- Node *owner = get_tree()->get_edited_scene_root();
-
- EditorUndoRedoManager *ur = EditorUndoRedoManager::get_singleton();
- ur->create_action(TTR("Create Navigation Mesh"));
-
- ur->add_do_method(node, "add_child", nmi, true);
- ur->add_do_method(nmi, "set_owner", owner);
- ur->add_do_method(Node3DEditor::get_singleton(), SceneStringName(_request_gizmo), nmi);
-
- ur->add_do_reference(nmi);
- ur->add_undo_method(node, "remove_child", nmi);
- ur->commit_action();
+ navigation_mesh_dialog->popup_centered(Vector2(200, 90));
} break;
case MENU_OPTION_CREATE_OUTLINE_MESH: {
@@ -472,6 +451,36 @@ void MeshInstance3DEditor::_debug_uv_draw() {
debug_uv->draw_multiline(uv_lines, get_theme_color(SNAME("mono_color"), EditorStringName(Editor)) * Color(1, 1, 1, 0.5));
}
+void MeshInstance3DEditor::_create_navigation_mesh() {
+ Ref<Mesh> mesh = node->get_mesh();
+ if (mesh.is_null()) {
+ return;
+ }
+
+ Ref<NavigationMesh> nmesh = memnew(NavigationMesh);
+
+ if (nmesh.is_null()) {
+ return;
+ }
+
+ nmesh->create_from_mesh(mesh);
+ NavigationRegion3D *nmi = memnew(NavigationRegion3D);
+ nmi->set_navigation_mesh(nmesh);
+
+ Node *owner = get_tree()->get_edited_scene_root();
+
+ EditorUndoRedoManager *ur = EditorUndoRedoManager::get_singleton();
+ ur->create_action(TTR("Create Navigation Mesh"));
+
+ ur->add_do_method(node, "add_child", nmi, true);
+ ur->add_do_method(nmi, "set_owner", owner);
+ ur->add_do_method(Node3DEditor::get_singleton(), SceneStringName(_request_gizmo), nmi);
+
+ ur->add_do_reference(nmi);
+ ur->add_undo_method(node, "remove_child", nmi);
+ ur->commit_action();
+}
+
void MeshInstance3DEditor::_create_outline_mesh() {
Ref<Mesh> mesh = node->get_mesh();
if (mesh.is_null()) {
@@ -608,6 +617,20 @@ MeshInstance3DEditor::MeshInstance3DEditor() {
debug_uv->set_custom_minimum_size(Size2(600, 600) * EDSCALE);
debug_uv->connect(SceneStringName(draw), callable_mp(this, &MeshInstance3DEditor::_debug_uv_draw));
debug_uv_dialog->add_child(debug_uv);
+
+ navigation_mesh_dialog = memnew(ConfirmationDialog);
+ navigation_mesh_dialog->set_title(TTR("Create NavigationMesh"));
+ navigation_mesh_dialog->set_ok_button_text(TTR("Create"));
+
+ VBoxContainer *navigation_mesh_dialog_vbc = memnew(VBoxContainer);
+ navigation_mesh_dialog->add_child(navigation_mesh_dialog_vbc);
+
+ Label *navigation_mesh_l = memnew(Label);
+ navigation_mesh_l->set_text(TTR("Before converting a rendering mesh to a navigation mesh, please verify:\n\n- The mesh is two-dimensional.\n- The mesh has no surface overlap.\n- The mesh has no self-intersection.\n- The mesh surfaces have indices.\n\nIf the mesh does not fulfill these requirements, the pathfinding will be broken."));
+ navigation_mesh_dialog_vbc->add_child(navigation_mesh_l);
+
+ add_child(navigation_mesh_dialog);
+ navigation_mesh_dialog->connect("confirmed", callable_mp(this, &MeshInstance3DEditor::_create_navigation_mesh));
}
void MeshInstance3DEditorPlugin::edit(Object *p_object) {
diff --git a/editor/plugins/mesh_instance_3d_editor_plugin.h b/editor/plugins/mesh_instance_3d_editor_plugin.h
index 20c151fb92..c982df9c5f 100644
--- a/editor/plugins/mesh_instance_3d_editor_plugin.h
+++ b/editor/plugins/mesh_instance_3d_editor_plugin.h
@@ -82,10 +82,13 @@ class MeshInstance3DEditor : public Control {
Control *debug_uv = nullptr;
Vector<Vector2> uv_lines;
+ ConfirmationDialog *navigation_mesh_dialog = nullptr;
+
void _create_collision_shape();
Vector<Ref<Shape3D>> create_shape_from_mesh(Ref<Mesh> p_mesh, int p_option, bool p_verbose);
void _menu_option(int p_option);
void _create_outline_mesh();
+ void _create_navigation_mesh();
void _create_uv_lines(int p_layer);
friend class MeshInstance3DEditorPlugin;
diff --git a/editor/plugins/node_3d_editor_plugin.cpp b/editor/plugins/node_3d_editor_plugin.cpp
index dc87bec584..d911e745db 100644
--- a/editor/plugins/node_3d_editor_plugin.cpp
+++ b/editor/plugins/node_3d_editor_plugin.cpp
@@ -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);
diff --git a/editor/plugins/polygon_2d_editor_plugin.cpp b/editor/plugins/polygon_2d_editor_plugin.cpp
index f0ea322504..dc99b07067 100644
--- a/editor/plugins/polygon_2d_editor_plugin.cpp
+++ b/editor/plugins/polygon_2d_editor_plugin.cpp
@@ -1387,7 +1387,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"));
diff --git a/editor/plugins/script_editor_plugin.cpp b/editor/plugins/script_editor_plugin.cpp
index 9da66a0862..213af652ca 100644
--- a/editor/plugins/script_editor_plugin.cpp
+++ b/editor/plugins/script_editor_plugin.cpp
@@ -4415,13 +4415,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/shader_editor_plugin.cpp b/editor/plugins/shader_editor_plugin.cpp
index 82c436d3a5..02cb76ac10 100644
--- a/editor/plugins/shader_editor_plugin.cpp
+++ b/editor/plugins/shader_editor_plugin.cpp
@@ -679,17 +679,17 @@ ShaderEditorPlugin::ShaderEditorPlugin() {
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_shortcut(ED_SHORTCUT("shader_editor/new", TTR("New Shader..."), KeyModifierMask::CMD_OR_CTRL | Key::N), FILE_NEW);
+ file_menu->get_popup()->add_shortcut(ED_SHORTCUT("shader_editor/new_include", TTR("New Shader Include..."), KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT | Key::N), 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/open", TTR("Load Shader File..."), KeyModifierMask::CMD_OR_CTRL | Key::O), FILE_OPEN);
+ file_menu->get_popup()->add_shortcut(ED_SHORTCUT("shader_editor/open_include", TTR("Load Shader Include File..."), KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT | Key::O), 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);
+ file_menu->get_popup()->add_shortcut(ED_SHORTCUT("shader_editor/close_file", TTR("Close File"), KeyModifierMask::CMD_OR_CTRL | Key::W), FILE_CLOSE);
file_menu->get_popup()->connect(SceneStringName(id_pressed), callable_mp(this, &ShaderEditorPlugin::_menu_item_pressed));
menu_hb->add_child(file_menu);
diff --git a/editor/plugins/sprite_frames_editor_plugin.cpp b/editor/plugins/sprite_frames_editor_plugin.cpp
index 48087e3166..ff5aca6cb0 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);
@@ -2290,6 +2367,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..d6345a3ac8 100644
--- a/editor/plugins/sprite_frames_editor_plugin.h
+++ b/editor/plugins/sprite_frames_editor_plugin.h
@@ -234,6 +234,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/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/visual_shader_editor_plugin.cpp b/editor/plugins/visual_shader_editor_plugin.cpp
index d50f5e51d5..c183aceb13 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()) {
@@ -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 &param_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;
@@ -4872,6 +4966,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 +5077,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 +5134,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;
@@ -5907,14 +6071,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 +6087,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 +6107,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 +6199,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 +6232,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 +6241,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,9 +6341,30 @@ 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"));
@@ -6196,22 +6398,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 +6422,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 +6501,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
///////////////////////////////////////
@@ -6591,14 +6865,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 +6886,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 +6905,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 +6947,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 +6965,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 +6987,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
@@ -7714,36 +7987,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(&params);
- 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(&params);
+ 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..b17036e39f 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();
@@ -414,6 +435,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 +615,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 +681,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 +692,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/main/main.cpp b/main/main.cpp
index f82df786bc..d76ddd5a66 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;
@@ -605,6 +606,9 @@ void Main::print_help(const char *p_binary) {
print_help_option("--gpu-abort", "Abort on graphics API usage errors (usually validation layer errors). May help see the problem if your system freezes.\n", CLI_OPTION_AVAILABILITY_TEMPLATE_DEBUG);
#endif
print_help_option("--generate-spirv-debug-info", "Generate SPIR-V debug information. This allows source-level shader debugging with RenderDoc.\n");
+#if defined(DEBUG_ENABLED) || defined(DEV_ENABLED)
+ print_help_option("--extra-gpu-memory-tracking", "Enables additional memory tracking (see class reference for `RenderingDevice.get_driver_and_device_memory_report()` and linked methods). Currently only implemented for Vulkan. Enabling this feature may cause crashes on some systems due to buggy drivers or bugs in the Vulkan Loader. See https://github.com/godotengine/godot/issues/95967\n");
+#endif
print_help_option("--remote-debug <uri>", "Remote debug (<protocol>://<host/IP>[:<port>], e.g. tcp://127.0.0.1:6007).\n");
print_help_option("--single-threaded-scene", "Force scene tree to run in single-threaded mode. Sub-thread groups are disabled and run on the main thread.\n");
#if defined(DEBUG_ENABLED)
@@ -1204,6 +1208,8 @@ Error Main::setup(const char *execpath, int argc, char *argv[], bool p_second_ph
#endif
} else if (arg == "--generate-spirv-debug-info") {
Engine::singleton->generate_spirv_debug_info = true;
+ } else if (arg == "--extra-gpu-memory-tracking") {
+ Engine::singleton->extra_gpu_memory_tracking = true;
} else if (arg == "--tablet-driver") {
if (N) {
tablet_driver = N->get();
@@ -2183,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;
}
}
@@ -2209,14 +2228,27 @@ 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");
goto error;
@@ -2687,6 +2719,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";
@@ -2702,7 +2735,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();
@@ -2722,6 +2755,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;
+ }
}
}
@@ -2744,6 +2782,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
@@ -2888,6 +2954,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) {
@@ -3948,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/misc/extension_api_validation/4.1-stable_4.2-stable.expected b/misc/extension_api_validation/4.1-stable_4.2-stable.expected
index 11cf8531c6..4b320e7216 100644
--- a/misc/extension_api_validation/4.1-stable_4.2-stable.expected
+++ b/misc/extension_api_validation/4.1-stable_4.2-stable.expected
@@ -1,10 +1,5 @@
-This file contains the expected output of --validate-extension-api when run against the extension_api.json of the
-4.1-stable tag (the basename of this file).
-
-Only lines that start with "Validate extension JSON:" matter, everything else is considered a comment and ignored. They
-should instead be used to justify these changes and describe how users should work around these changes.
-
-Add new entries at the end of the file.
+This file contains, when concatenated to the expected output since 4.2, the expected output of --validate-extension-api
+when run against the extension_api.json of the 4.1-stable tag (first part of the basename of this file).
## Changes between 4.1-stable and 4.2-stable
diff --git a/misc/extension_api_validation/4.2-stable_4.3-stable.expected b/misc/extension_api_validation/4.2-stable_4.3-stable.expected
index ce8f24c7a9..b6ad086792 100644
--- a/misc/extension_api_validation/4.2-stable_4.3-stable.expected
+++ b/misc/extension_api_validation/4.2-stable_4.3-stable.expected
@@ -1,10 +1,5 @@
-This file contains the expected output of --validate-extension-api when run against the extension_api.json of the
-4.2-stable tag (the basename of this file).
-
-Only lines that start with "Validate extension JSON:" matter, everything else is considered a comment and ignored. They
-should instead be used to justify these changes and describe how users should work around these changes.
-
-Add new entries at the end of the file.
+This file contains, when concatenated to the expected output since 4.3, the expected output of --validate-extension-api
+when run against the extension_api.json of the 4.2-stable tag (first part of the basename of this file).
## Changes between 4.2-stable and 4.3-stable
diff --git a/misc/scripts/validate_extension_api.sh b/misc/scripts/validate_extension_api.sh
index 88c5d82374..1d64883541 100755
--- a/misc/scripts/validate_extension_api.sh
+++ b/misc/scripts/validate_extension_api.sh
@@ -71,7 +71,9 @@ while read -r file; do
obsolete_validation_error="$(comm -13 "$validation_output" "$allowed_errors")"
if [ -n "$obsolete_validation_error" ] && [ "$warn_extra" = "1" ]; then
- make_annotation "The following validation errors no longer occur (compared to $reference_tag):" "$obsolete_validation_error" warning "$file"
+ #make_annotation "The following validation errors no longer occur (compared to $reference_tag):" "$obsolete_validation_error" warning "$file"
+ echo "The following validation errors no longer occur (compared to $reference_tag):"
+ echo "$obsolete_validation_error"
fi
if [ -n "$new_validation_error" ]; then
make_annotation "Compatibility to $reference_tag is broken in the following ways:" "$new_validation_error" error "$file"
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_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..a6fd3f94da 100644
--- a/modules/gdscript/gdscript_editor.cpp
+++ b/modules/gdscript/gdscript_editor.cpp
@@ -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) {
diff --git a/modules/lightmapper_rd/lightmapper_rd.cpp b/modules/lightmapper_rd/lightmapper_rd.cpp
index 50b552d733..ddc51bcf6b 100644
--- a/modules/lightmapper_rd/lightmapper_rd.cpp
+++ b/modules/lightmapper_rd/lightmapper_rd.cpp
@@ -915,7 +915,7 @@ LightmapperRD::BakeError LightmapperRD::_denoise_oidn(RenderingDevice *p_rd, RID
return BAKE_OK;
}
-LightmapperRD::BakeError LightmapperRD::_denoise(RenderingDevice *p_rd, Ref<RDShaderFile> &p_compute_shader, const RID &p_compute_base_uniform_set, PushConstant &p_push_constant, RID p_source_light_tex, RID p_source_normal_tex, RID p_dest_light_tex, float p_denoiser_strength, int p_denoiser_range, const Size2i &p_atlas_size, int p_atlas_slices, bool p_bake_sh, BakeStepFunc p_step_function) {
+LightmapperRD::BakeError LightmapperRD::_denoise(RenderingDevice *p_rd, Ref<RDShaderFile> &p_compute_shader, const RID &p_compute_base_uniform_set, PushConstant &p_push_constant, RID p_source_light_tex, RID p_source_normal_tex, RID p_dest_light_tex, float p_denoiser_strength, int p_denoiser_range, const Size2i &p_atlas_size, int p_atlas_slices, bool p_bake_sh, BakeStepFunc p_step_function, void *p_bake_userdata) {
RID denoise_params_buffer = p_rd->uniform_buffer_create(sizeof(DenoiseParams));
DenoiseParams denoise_params;
denoise_params.spatial_bandwidth = 5.0f;
@@ -978,6 +978,11 @@ LightmapperRD::BakeError LightmapperRD::_denoise(RenderingDevice *p_rd, Ref<RDSh
p_rd->sync();
}
}
+ if (p_step_function) {
+ int percent = (s + 1) * 100 / p_atlas_slices;
+ float p = float(s) / p_atlas_slices * 0.1;
+ p_step_function(0.8 + p, vformat(RTR("Denoising %d%%"), percent), p_bake_userdata, false);
+ }
}
p_rd->free(compute_shader_denoise);
@@ -1581,6 +1586,14 @@ LightmapperRD::BakeError LightmapperRD::bake(BakeQuality p_quality, bool p_use_d
Ref<Image> img = Image::create_from_data(atlas_size.width, atlas_size.height, false, Image::FORMAT_RGBAH, s);
img->save_exr("res://2_light_primary_" + itos(i) + ".exr", false);
}
+
+ if (p_bake_sh) {
+ for (int i = 0; i < atlas_slices * 4; i++) {
+ Vector<uint8_t> s = rd->texture_get_data(light_accum_tex, i);
+ Ref<Image> img = Image::create_from_data(atlas_size.width, atlas_size.height, false, Image::FORMAT_RGBAH, s);
+ img->save_exr("res://2_light_primary_accum_" + itos(i) + ".exr", false);
+ }
+ }
#endif
/* SECONDARY (indirect) LIGHT PASS(ES) */
@@ -1804,7 +1817,7 @@ LightmapperRD::BakeError LightmapperRD::bake(BakeQuality p_quality, bool p_use_d
} else {
// JNLM (built-in).
SWAP(light_accum_tex, light_accum_tex2);
- error = _denoise(rd, compute_shader, compute_base_uniform_set, push_constant, light_accum_tex2, normal_tex, light_accum_tex, p_denoiser_strength, p_denoiser_range, atlas_size, atlas_slices, p_bake_sh, p_step_function);
+ error = _denoise(rd, compute_shader, compute_base_uniform_set, push_constant, light_accum_tex2, normal_tex, light_accum_tex, p_denoiser_strength, p_denoiser_range, atlas_size, atlas_slices, p_bake_sh, p_step_function, p_bake_userdata);
}
if (unlikely(error != BAKE_OK)) {
return error;
diff --git a/modules/lightmapper_rd/lightmapper_rd.h b/modules/lightmapper_rd/lightmapper_rd.h
index 487c44a480..59c2d52e69 100644
--- a/modules/lightmapper_rd/lightmapper_rd.h
+++ b/modules/lightmapper_rd/lightmapper_rd.h
@@ -272,7 +272,7 @@ class LightmapperRD : public Lightmapper {
void _raster_geometry(RenderingDevice *rd, Size2i atlas_size, int atlas_slices, int grid_size, AABB bounds, float p_bias, Vector<int> slice_triangle_count, RID position_tex, RID unocclude_tex, RID normal_tex, RID raster_depth_buffer, RID rasterize_shader, RID raster_base_uniform);
BakeError _dilate(RenderingDevice *rd, Ref<RDShaderFile> &compute_shader, RID &compute_base_uniform_set, PushConstant &push_constant, RID &source_light_tex, RID &dest_light_tex, const Size2i &atlas_size, int atlas_slices);
- BakeError _denoise(RenderingDevice *p_rd, Ref<RDShaderFile> &p_compute_shader, const RID &p_compute_base_uniform_set, PushConstant &p_push_constant, RID p_source_light_tex, RID p_source_normal_tex, RID p_dest_light_tex, float p_denoiser_strength, int p_denoiser_range, const Size2i &p_atlas_size, int p_atlas_slices, bool p_bake_sh, BakeStepFunc p_step_function);
+ BakeError _denoise(RenderingDevice *p_rd, Ref<RDShaderFile> &p_compute_shader, const RID &p_compute_base_uniform_set, PushConstant &p_push_constant, RID p_source_light_tex, RID p_source_normal_tex, RID p_dest_light_tex, float p_denoiser_strength, int p_denoiser_range, const Size2i &p_atlas_size, int p_atlas_slices, bool p_bake_sh, BakeStepFunc p_step_function, void *p_bake_userdata);
Error _store_pfm(RenderingDevice *p_rd, RID p_atlas_tex, int p_index, const Size2i &p_atlas_size, const String &p_name);
Ref<Image> _read_pfm(const String &p_name);
diff --git a/modules/lightmapper_rd/lm_compute.glsl b/modules/lightmapper_rd/lm_compute.glsl
index 8968ff0b57..88fc316679 100644
--- a/modules/lightmapper_rd/lm_compute.glsl
+++ b/modules/lightmapper_rd/lm_compute.glsl
@@ -649,15 +649,20 @@ void main() {
light_for_texture += light;
#ifdef USE_SH_LIGHTMAPS
+ // These coefficients include the factored out SH evaluation, diffuse convolution, and final application, as well as the BRDF 1/PI and the spherical monte carlo factor.
+ // LO: 1/(2*sqrtPI) * 1/(2*sqrtPI) * PI * PI * 1/PI = 0.25
+ // L1: sqrt(3/(4*pi)) * sqrt(3/(4*pi)) * (PI*2/3) * (2 * PI) * 1/PI = 1.0
+ // Note: This only works because we aren't scaling, rotating, or combing harmonics, we are just directing applying them in the shader.
+
float c[4] = float[](
- 0.282095, //l0
- 0.488603 * light_dir.y, //l1n1
- 0.488603 * light_dir.z, //l1n0
- 0.488603 * light_dir.x //l1p1
+ 0.25, //l0
+ light_dir.y, //l1n1
+ light_dir.z, //l1n0
+ light_dir.x //l1p1
);
for (uint j = 0; j < 4; j++) {
- sh_accum[j].rgb += light * c[j] * 8.0;
+ sh_accum[j].rgb += light * c[j] * bake_params.exposure_normalization;
}
#endif
}
@@ -710,15 +715,20 @@ void main() {
vec3 light = trace_indirect_light(position, ray_dir, noise, texel_size_world_space);
#ifdef USE_SH_LIGHTMAPS
+ // These coefficients include the factored out SH evaluation, diffuse convolution, and final application, as well as the BRDF 1/PI and the spherical monte carlo factor.
+ // LO: 1/(2*sqrtPI) * 1/(2*sqrtPI) * PI * PI * 1/PI = 0.25
+ // L1: sqrt(3/(4*pi)) * sqrt(3/(4*pi)) * (PI*2/3) * (2 * PI) * 1/PI = 1.0
+ // Note: This only works because we aren't scaling, rotating, or combing harmonics, we are just directing applying them in the shader.
+
float c[4] = float[](
- 0.282095, //l0
- 0.488603 * ray_dir.y, //l1n1
- 0.488603 * ray_dir.z, //l1n0
- 0.488603 * ray_dir.x //l1p1
+ 0.25, //l0
+ ray_dir.y, //l1n1
+ ray_dir.z, //l1n0
+ ray_dir.x //l1p1
);
for (uint j = 0; j < 4; j++) {
- sh_accum[j].rgb += light * c[j] * 8.0;
+ sh_accum[j].rgb += light * c[j];
}
#else
light_accum += light;
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/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/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/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/extensions/openxr_hand_tracking_extension.cpp b/modules/openxr/extensions/openxr_hand_tracking_extension.cpp
index 6d6231f6fa..ea64f077c5 100644
--- a/modules/openxr/extensions/openxr_hand_tracking_extension.cpp
+++ b/modules/openxr/extensions/openxr_hand_tracking_extension.cpp
@@ -297,7 +297,7 @@ void OpenXRHandTrackingExtension::on_process() {
godot_tracker->set_hand_joint_radius((XRHandTracker::HandJoint)joint, location.radius);
if (joint == XR_HAND_JOINT_PALM_EXT) {
- if (location.locationFlags & XR_SPACE_LOCATION_POSITION_TRACKED_BIT) {
+ if (location.locationFlags & XR_SPACE_LOCATION_POSITION_VALID_BIT) {
XrHandTrackingDataSourceStateEXT &data_source = hand_trackers[i].data_source;
XRHandTracker::HandTrackingSource source = XRHandTracker::HAND_TRACKING_SOURCE_UNKNOWN;
diff --git a/modules/openxr/openxr_api.cpp b/modules/openxr/openxr_api.cpp
index db5c04fdef..e4ec318a42 100644
--- a/modules/openxr/openxr_api.cpp
+++ b/modules/openxr/openxr_api.cpp
@@ -1737,8 +1737,12 @@ void OpenXRAPI::cleanup_extension_wrappers() {
XrHandTrackerEXT OpenXRAPI::get_hand_tracker(int p_hand_index) {
ERR_FAIL_INDEX_V(p_hand_index, OpenXRHandTrackingExtension::HandTrackedHands::OPENXR_MAX_TRACKED_HANDS, XR_NULL_HANDLE);
+
+ OpenXRHandTrackingExtension *hand_tracking = OpenXRHandTrackingExtension::get_singleton();
+ ERR_FAIL_NULL_V(hand_tracking, XR_NULL_HANDLE);
+
OpenXRHandTrackingExtension::HandTrackedHands hand = static_cast<OpenXRHandTrackingExtension::HandTrackedHands>(p_hand_index);
- return OpenXRHandTrackingExtension::get_singleton()->get_hand_tracker(hand)->hand_tracker;
+ return hand_tracking->get_hand_tracker(hand)->hand_tracker;
}
Size2 OpenXRAPI::get_recommended_target_size() {
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/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/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..0fdaca4839 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[] = {
@@ -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;
@@ -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;
@@ -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;
}
}
diff --git a/platform/android/file_access_android.h b/platform/android/file_access_android.h
index e79daeafb3..b465a92c78 100644
--- a/platform/android/file_access_android.h
+++ b/platform/android/file_access_android.h
@@ -86,7 +86,7 @@ public:
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..9ae48dfb10 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;
@@ -331,19 +325,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..2795ac02ac 100644
--- a/platform/android/file_access_filesystem_jandroid.h
+++ b/platform/android/file_access_filesystem_jandroid.h
@@ -101,7 +101,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..f9a3e10680 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 {
diff --git a/platform/android/java/editor/src/main/assets/keystores/debug.keystore b/platform/android/java/editor/src/main/assets/keystores/debug.keystore
new file mode 100644
index 0000000000..3b7a97c8ee
--- /dev/null
+++ b/platform/android/java/editor/src/main/assets/keystores/debug.keystore
Binary files differ
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/GodotEditor.kt b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt
index 7c11d69609..9cc133046b 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
@@ -43,8 +43,11 @@ 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.*
@@ -350,4 +353,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/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/lib/src/org/godotengine/godot/Godot.kt b/platform/android/java/lib/src/org/godotengine/godot/Godot.kt
index 111cd48405..49e8ffb008 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/Godot.kt
+++ b/platform/android/java/lib/src/org/godotengine/godot/Godot.kt
@@ -50,6 +50,7 @@ import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsAnimationCompat
import androidx.core.view.WindowInsetsCompat
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 +84,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
@@ -834,11 +839,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 +1054,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/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_godot_lib_jni.cpp b/platform/android/java_godot_lib_jni.cpp
index a4a425f685..1114969de8 100644
--- a/platform/android/java_godot_lib_jni.cpp
+++ b/platform/android/java_godot_lib_jni.cpp
@@ -574,4 +574,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..f1759af54a 100644
--- a/platform/android/java_godot_wrapper.cpp
+++ b/platform/android/java_godot_wrapper.cpp
@@ -84,6 +84,8 @@ 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");
}
GodotJavaWrapper::~GodotJavaWrapper() {
@@ -424,3 +426,42 @@ 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;
+ }
+}
diff --git a/platform/android/java_godot_wrapper.h b/platform/android/java_godot_wrapper.h
index 358cf3261d..6b66565981 100644
--- a/platform/android/java_godot_wrapper.h
+++ b/platform/android/java_godot_wrapper.h
@@ -75,6 +75,8 @@ private:
jmethodID _end_benchmark_measure = nullptr;
jmethodID _dump_benchmark = nullptr;
jmethodID _has_feature = nullptr;
+ jmethodID _sign_apk = nullptr;
+ jmethodID _verify_apk = nullptr;
public:
GodotJavaWrapper(JNIEnv *p_env, jobject p_activity, jobject p_godot_instance);
@@ -116,6 +118,10 @@ 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);
};
#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/linuxbsd/wayland/wayland_thread.h b/platform/linuxbsd/wayland/wayland_thread.h
index 6fd7a60966..84e9bdc2dc 100644
--- a/platform/linuxbsd/wayland/wayland_thread.h
+++ b/platform/linuxbsd/wayland/wayland_thread.h
@@ -44,7 +44,7 @@
#include <wayland-client-core.h>
#include <wayland-cursor.h>
#ifdef GLES3_ENABLED
-#include <wayland-egl.h>
+#include <wayland-egl-core.h>
#endif
#include <xkbcommon/xkbcommon.h>
#endif // SOWRAP_ENABLED
diff --git a/platform/macos/display_server_macos.mm b/platform/macos/display_server_macos.mm
index 3e0a5efe52..989a9dcf6c 100644
--- a/platform/macos/display_server_macos.mm
+++ b/platform/macos/display_server_macos.mm
@@ -3609,7 +3609,11 @@ DisplayServerMacOS::DisplayServerMacOS(const String &p_rendering_driver, WindowM
gl_manager_angle = nullptr;
bool fallback = GLOBAL_GET("rendering/gl_compatibility/fallback_to_native");
if (fallback) {
- WARN_PRINT("Your video card drivers seem not to support the required Metal version, switching to native OpenGL.");
+#ifdef EGL_STATIC
+ WARN_PRINT("Your video card drivers seem not to support GLES3 / ANGLE, switching to native OpenGL.");
+#else
+ 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";
} else {
r_error = ERR_UNAVAILABLE;
diff --git a/platform/web/audio_driver_web.cpp b/platform/web/audio_driver_web.cpp
index 5e046d7050..0108f40726 100644
--- a/platform/web/audio_driver_web.cpp
+++ b/platform/web/audio_driver_web.cpp
@@ -294,6 +294,7 @@ void AudioDriverWeb::start_sample_playback(const Ref<AudioSamplePlayback> &p_pla
itos(p_playback->stream->get_instance_id()).utf8().get_data(),
AudioServer::get_singleton()->get_bus_index(p_playback->bus),
p_playback->offset,
+ p_playback->pitch_scale,
volume_ptrw);
}
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/godot_audio.h b/platform/web/godot_audio.h
index 4961ebd2bb..f5a2a85605 100644
--- a/platform/web/godot_audio.h
+++ b/platform/web/godot_audio.h
@@ -51,7 +51,7 @@ extern void godot_audio_input_stop();
extern int godot_audio_sample_stream_is_registered(const char *p_stream_object_id);
extern void godot_audio_sample_register_stream(const char *p_stream_object_id, float *p_frames_buf, int p_frames_total, const char *p_loop_mode, int p_loop_begin, int p_loop_end);
extern void godot_audio_sample_unregister_stream(const char *p_stream_object_id);
-extern void godot_audio_sample_start(const char *p_playback_object_id, const char *p_stream_object_id, int p_bus_index, float p_offset, float *p_volume_ptr);
+extern void godot_audio_sample_start(const char *p_playback_object_id, const char *p_stream_object_id, int p_bus_index, float p_offset, float p_pitch_scale, float *p_volume_ptr);
extern void godot_audio_sample_stop(const char *p_playback_object_id);
extern void godot_audio_sample_set_pause(const char *p_playback_object_id, bool p_pause);
extern int godot_audio_sample_is_active(const char *p_playback_object_id);
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/web/js/libs/library_godot_audio.js b/platform/web/js/libs/library_godot_audio.js
index 0ba6eed464..40fb0c356c 100644
--- a/platform/web/js/libs/library_godot_audio.js
+++ b/platform/web/js/libs/library_godot_audio.js
@@ -328,6 +328,7 @@ class SampleNodeBus {
* offset?: number
* playbackRate?: number
* startTime?: number
+ * pitchScale?: number
* loopMode?: LoopMode
* volume?: Float32Array
* start?: boolean
@@ -438,7 +439,7 @@ class SampleNode {
/** @type {LoopMode} */
this.loopMode = options.loopMode ?? this.getSample().loopMode ?? 'disabled';
/** @type {number} */
- this._pitchScale = 1;
+ this._pitchScale = options.pitchScale ?? 1;
/** @type {number} */
this._sourceStartTime = 0;
/** @type {Map<Bus, SampleNodeBus>} */
@@ -1648,13 +1649,14 @@ const _GodotAudio = {
},
godot_audio_sample_start__proxy: 'sync',
- godot_audio_sample_start__sig: 'viiiii',
+ godot_audio_sample_start__sig: 'viiiifi',
/**
* Starts a sample.
* @param {number} playbackObjectIdStrPtr Playback object id pointer
* @param {number} streamObjectIdStrPtr Stream object id pointer
* @param {number} busIndex Bus index
* @param {number} offset Sample offset
+ * @param {number} pitchScale Pitch scale
* @param {number} volumePtr Volume pointer
* @returns {void}
*/
@@ -1663,6 +1665,7 @@ const _GodotAudio = {
streamObjectIdStrPtr,
busIndex,
offset,
+ pitchScale,
volumePtr
) {
/** @type {string} */
@@ -1671,11 +1674,12 @@ const _GodotAudio = {
const streamObjectId = GodotRuntime.parseString(streamObjectIdStrPtr);
/** @type {Float32Array} */
const volume = GodotRuntime.heapSub(HEAPF32, volumePtr, 8);
- /** @type {SampleNodeConstructorOptions} */
+ /** @type {SampleNodeOptions} */
const startOptions = {
offset,
volume,
playbackRate: 1,
+ pitchScale,
start: true,
};
GodotAudio.start_sample(
diff --git a/platform/windows/display_server_windows.cpp b/platform/windows/display_server_windows.cpp
index b55eda0e51..6fa3f2c9d6 100644
--- a/platform/windows/display_server_windows.cpp
+++ b/platform/windows/display_server_windows.cpp
@@ -6183,10 +6183,12 @@ DisplayServerWindows::DisplayServerWindows(const String &p_rendering_driver, Win
#endif
}
+ bool gl_supported = true;
if (fallback && (rendering_driver == "opengl3")) {
Dictionary gl_info = detect_wgl();
bool force_angle = false;
+ gl_supported = gl_info["version"].operator int() >= 30003;
Vector2i device_id = _get_device_ids(gl_info["name"]);
Array device_list = GLOBAL_GET("rendering/gl_compatibility/force_angle_on_devices");
@@ -6210,12 +6212,37 @@ DisplayServerWindows::DisplayServerWindows(const String &p_rendering_driver, Win
if (force_angle || (gl_info["version"].operator int() < 30003)) {
tested_drivers.set_flag(DRIVER_ID_COMPAT_OPENGL3);
if (show_warning) {
- WARN_PRINT("Your video card drivers seem not to support the required OpenGL 3.3 version, switching to ANGLE.");
+ if (gl_info["version"].operator int() < 30003) {
+ WARN_PRINT("Your video card drivers seem not to support the required OpenGL 3.3 version, switching to ANGLE.");
+ } else {
+ WARN_PRINT("Your video card drivers are known to have low quality OpenGL 3.3 support, switching to ANGLE.");
+ }
}
rendering_driver = "opengl3_angle";
}
}
+ if (rendering_driver == "opengl3_angle") {
+ gl_manager_angle = memnew(GLManagerANGLE_Windows);
+ tested_drivers.set_flag(DRIVER_ID_COMPAT_ANGLE_D3D11);
+
+ if (gl_manager_angle->initialize() != OK) {
+ memdelete(gl_manager_angle);
+ gl_manager_angle = nullptr;
+ bool fallback_to_native = GLOBAL_GET("rendering/gl_compatibility/fallback_to_native");
+ if (fallback_to_native && gl_supported) {
+#ifdef EGL_STATIC
+ WARN_PRINT("Your video card drivers seem not to support GLES3 / ANGLE, switching to native OpenGL.");
+#else
+ WARN_PRINT("Your video card drivers seem not to support GLES3 / ANGLE or ANGLE dynamic libraries (libEGL.dll and libGLESv2.dll) are missing, switching to native OpenGL.");
+#endif
+ rendering_driver = "opengl3";
+ } else {
+ r_error = ERR_UNAVAILABLE;
+ ERR_FAIL_MSG("Could not initialize ANGLE OpenGL.");
+ }
+ }
+ }
if (rendering_driver == "opengl3") {
gl_manager_native = memnew(GLManagerNative_Windows);
tested_drivers.set_flag(DRIVER_ID_COMPAT_OPENGL3);
@@ -6224,26 +6251,17 @@ DisplayServerWindows::DisplayServerWindows(const String &p_rendering_driver, Win
memdelete(gl_manager_native);
gl_manager_native = nullptr;
r_error = ERR_UNAVAILABLE;
- return;
+ ERR_FAIL_MSG("Could not initialize native OpenGL.");
}
+ }
+ if (rendering_driver == "opengl3") {
RasterizerGLES3::make_current(true);
}
if (rendering_driver == "opengl3_angle") {
- gl_manager_angle = memnew(GLManagerANGLE_Windows);
- tested_drivers.set_flag(DRIVER_ID_COMPAT_ANGLE_D3D11);
-
- if (gl_manager_angle->initialize() != OK) {
- memdelete(gl_manager_angle);
- gl_manager_angle = nullptr;
- r_error = ERR_UNAVAILABLE;
- return;
- }
-
RasterizerGLES3::make_current(false);
}
#endif
-
String appname;
if (Engine::get_singleton()->is_editor_hint()) {
appname = "Godot.GodotEditor." + String(VERSION_FULL_CONFIG);
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/audio/audio_stream_player_internal.cpp b/scene/audio/audio_stream_player_internal.cpp
index 206408e3a7..7d1ed56ca8 100644
--- a/scene/audio/audio_stream_player_internal.cpp
+++ b/scene/audio/audio_stream_player_internal.cpp
@@ -152,6 +152,7 @@ Ref<AudioStreamPlayback> AudioStreamPlayerInternal::play_basic() {
Ref<AudioSamplePlayback> sample_playback;
sample_playback.instantiate();
sample_playback->stream = stream;
+ sample_playback->pitch_scale = pitch_scale;
stream_playback->set_sample_playback(sample_playback);
}
} else if (!stream->is_meta_stream()) {
diff --git a/scene/gui/control.cpp b/scene/gui/control.cpp
index b95df86e50..2003471de0 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() {
diff --git a/scene/gui/menu_button.cpp b/scene/gui/menu_button.cpp
index c60f728f34..8c5bb1b33d 100644
--- a/scene/gui/menu_button.cpp
+++ b/scene/gui/menu_button.cpp
@@ -198,7 +198,7 @@ void MenuButton::_bind_methods() {
base_property_helper.register_property(PropertyInfo(Variant::OBJECT, "icon", PROPERTY_HINT_RESOURCE_TYPE, "Texture2D"), defaults.icon);
base_property_helper.register_property(PropertyInfo(Variant::INT, "checkable", PROPERTY_HINT_ENUM, "No,As Checkbox,As Radio Button"), defaults.checkable_type);
base_property_helper.register_property(PropertyInfo(Variant::BOOL, "checked"), defaults.checked);
- base_property_helper.register_property(PropertyInfo(Variant::INT, "id", PROPERTY_HINT_RANGE, "0,10,1,or_greater"), defaults.id);
+ base_property_helper.register_property(PropertyInfo(Variant::INT, "id", PROPERTY_HINT_RANGE, "0,10,1,or_greater", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_STORE_IF_NULL), defaults.id);
base_property_helper.register_property(PropertyInfo(Variant::BOOL, "disabled"), defaults.disabled);
base_property_helper.register_property(PropertyInfo(Variant::BOOL, "separator"), defaults.separator);
PropertyListHelper::register_base_helper(&base_property_helper);
diff --git a/scene/gui/option_button.cpp b/scene/gui/option_button.cpp
index a1425fb847..5432058f7b 100644
--- a/scene/gui/option_button.cpp
+++ b/scene/gui/option_button.cpp
@@ -574,7 +574,7 @@ void OptionButton::_bind_methods() {
base_property_helper.set_array_length_getter(&OptionButton::get_item_count);
base_property_helper.register_property(PropertyInfo(Variant::STRING, "text"), defaults.text, &OptionButton::_dummy_setter, &OptionButton::get_item_text);
base_property_helper.register_property(PropertyInfo(Variant::OBJECT, "icon", PROPERTY_HINT_RESOURCE_TYPE, "Texture2D"), defaults.icon, &OptionButton::_dummy_setter, &OptionButton::get_item_icon);
- base_property_helper.register_property(PropertyInfo(Variant::INT, "id", PROPERTY_HINT_RANGE, "0,10,1,or_greater"), defaults.id, &OptionButton::_dummy_setter, &OptionButton::get_item_id);
+ base_property_helper.register_property(PropertyInfo(Variant::INT, "id", PROPERTY_HINT_RANGE, "0,10,1,or_greater", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_STORE_IF_NULL), defaults.id, &OptionButton::_dummy_setter, &OptionButton::get_item_id);
base_property_helper.register_property(PropertyInfo(Variant::BOOL, "disabled"), defaults.disabled, &OptionButton::_dummy_setter, &OptionButton::is_item_disabled);
base_property_helper.register_property(PropertyInfo(Variant::BOOL, "separator"), defaults.separator, &OptionButton::_dummy_setter, &OptionButton::is_item_separator);
PropertyListHelper::register_base_helper(&base_property_helper);
diff --git a/scene/gui/popup_menu.cpp b/scene/gui/popup_menu.cpp
index 86b0165754..5de4cdaf59 100644
--- a/scene/gui/popup_menu.cpp
+++ b/scene/gui/popup_menu.cpp
@@ -2823,7 +2823,7 @@ void PopupMenu::_bind_methods() {
base_property_helper.register_property(PropertyInfo(Variant::OBJECT, "icon", PROPERTY_HINT_RESOURCE_TYPE, "Texture2D"), defaults.icon, &PopupMenu::set_item_icon, &PopupMenu::get_item_icon);
base_property_helper.register_property(PropertyInfo(Variant::INT, "checkable", PROPERTY_HINT_ENUM, "No,As checkbox,As radio button"), defaults.checkable_type, &PopupMenu::_set_item_checkable_type, &PopupMenu::_get_item_checkable_type);
base_property_helper.register_property(PropertyInfo(Variant::BOOL, "checked"), defaults.checked, &PopupMenu::set_item_checked, &PopupMenu::is_item_checked);
- base_property_helper.register_property(PropertyInfo(Variant::INT, "id", PROPERTY_HINT_RANGE, "0,10,1,or_greater"), defaults.id, &PopupMenu::set_item_id, &PopupMenu::get_item_id);
+ base_property_helper.register_property(PropertyInfo(Variant::INT, "id", PROPERTY_HINT_RANGE, "0,10,1,or_greater", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_STORE_IF_NULL), defaults.id, &PopupMenu::set_item_id, &PopupMenu::get_item_id);
base_property_helper.register_property(PropertyInfo(Variant::BOOL, "disabled"), defaults.disabled, &PopupMenu::set_item_disabled, &PopupMenu::is_item_disabled);
base_property_helper.register_property(PropertyInfo(Variant::BOOL, "separator"), defaults.separator, &PopupMenu::set_item_as_separator, &PopupMenu::is_item_separator);
PropertyListHelper::register_base_helper(&base_property_helper);
diff --git a/scene/gui/rich_text_label.cpp b/scene/gui/rich_text_label.cpp
index e9fe78e162..457392fb2c 100644
--- a/scene/gui/rich_text_label.cpp
+++ b/scene/gui/rich_text_label.cpp
@@ -1905,7 +1905,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();
@@ -2796,7 +2796,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()) {
@@ -6002,7 +6002,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);
diff --git a/scene/gui/rich_text_label.h b/scene/gui/rich_text_label.h
index 83285bd7cd..9f81674454 100644
--- a/scene/gui/rich_text_label.h
+++ b/scene/gui/rich_text_label.h
@@ -785,7 +785,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);
diff --git a/scene/main/viewport.cpp b/scene/main/viewport.cpp
index 0036247625..192427f8a6 100644
--- a/scene/main/viewport.cpp
+++ b/scene/main/viewport.cpp
@@ -1933,7 +1933,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/property_list_helper.cpp b/scene/property_list_helper.cpp
index f840aaa759..c6c21e0dba 100644
--- a/scene/property_list_helper.cpp
+++ b/scene/property_list_helper.cpp
@@ -142,7 +142,7 @@ void PropertyListHelper::get_property_list(List<PropertyInfo> *p_list) const {
const Property &property = E.value;
PropertyInfo info = property.info;
- if (_call_getter(&property, i) == property.default_value) {
+ if (!(info.usage & PROPERTY_USAGE_STORE_IF_NULL) && _call_getter(&property, i) == property.default_value) {
info.usage &= (~PROPERTY_USAGE_STORAGE);
}
diff --git a/scene/resources/audio_stream_wav.cpp b/scene/resources/audio_stream_wav.cpp
index e2ac0e6d26..de6a069567 100644
--- a/scene/resources/audio_stream_wav.cpp
+++ b/scene/resources/audio_stream_wav.cpp
@@ -560,6 +560,7 @@ double AudioStreamWAV::get_length() const {
qoa_desc desc = { 0, 0, 0, { { { 0 }, { 0 } } } };
qoa_decode_header((uint8_t *)data + DATA_PAD, data_bytes, &desc);
len = desc.samples * desc.channels;
+ break;
}
if (stereo) {
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/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/visual_shader.cpp b/scene/resources/visual_shader.cpp
index 1fa52b9c73..a144c5ba83 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"
@@ -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/";
@@ -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,244 @@ 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_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_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_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 +3336,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 +3353,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
diff --git a/scene/resources/visual_shader.h b/scene/resources/visual_shader.h
index 9cd8f86d0f..2b213948de 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,
@@ -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/servers/audio/audio_stream.h b/servers/audio/audio_stream.h
index d02dc6aae7..c41545aeba 100644
--- a/servers/audio/audio_stream.h
+++ b/servers/audio/audio_stream.h
@@ -50,6 +50,7 @@ public:
Ref<AudioStream> stream;
float offset = 0.0f;
+ float pitch_scale = 1.0;
Vector<AudioFrame> volume_vector;
StringName bus;
};
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 c2d36adc92..1420e7939a 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
@@ -1532,16 +1532,10 @@ void fragment_shader(in SceneData scene_data) {
vec3 n = normalize(lightmaps.data[ofs].normal_xform * normal);
float en = lightmaps.data[ofs].exposure_normalization;
- ambient_light += lm_light_l0 * 0.282095f * en;
- ambient_light += lm_light_l1n1 * 0.32573 * n.y * en;
- ambient_light += lm_light_l1_0 * 0.32573 * n.z * en;
- ambient_light += lm_light_l1p1 * 0.32573 * n.x * en;
- if (metallic > 0.01) { // since the more direct bounced light is lost, we can kind of fake it with this trick
- vec3 r = reflect(normalize(-vertex), normal);
- specular_light += lm_light_l1n1 * 0.32573 * r.y * en;
- specular_light += lm_light_l1_0 * 0.32573 * r.z * en;
- specular_light += lm_light_l1p1 * 0.32573 * r.x * en;
- }
+ ambient_light += lm_light_l0 * en;
+ ambient_light += lm_light_l1n1 * n.y * en;
+ ambient_light += lm_light_l1_0 * n.z * en;
+ ambient_light += lm_light_l1p1 * n.x * en;
} else {
if (sc_use_lightmap_bicubic_filter) {
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 4e1da64151..90947aca80 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
@@ -1291,17 +1291,10 @@ void main() {
vec3 n = normalize(lightmaps.data[ofs].normal_xform * normal);
float exposure_normalization = lightmaps.data[ofs].exposure_normalization;
- ambient_light += lm_light_l0 * 0.282095f;
- ambient_light += lm_light_l1n1 * 0.32573 * n.y * exposure_normalization;
- ambient_light += lm_light_l1_0 * 0.32573 * n.z * exposure_normalization;
- ambient_light += lm_light_l1p1 * 0.32573 * n.x * exposure_normalization;
- if (metallic > 0.01) { // since the more direct bounced light is lost, we can kind of fake it with this trick
- vec3 r = reflect(normalize(-vertex), normal);
- specular_light += lm_light_l1n1 * 0.32573 * r.y * exposure_normalization;
- specular_light += lm_light_l1_0 * 0.32573 * r.z * exposure_normalization;
- specular_light += lm_light_l1p1 * 0.32573 * r.x * exposure_normalization;
- }
-
+ ambient_light += lm_light_l0 * exposure_normalization;
+ ambient_light += lm_light_l1n1 * n.y * exposure_normalization;
+ ambient_light += lm_light_l1_0 * n.z * exposure_normalization;
+ ambient_light += lm_light_l1p1 * n.x * exposure_normalization;
} else {
if (sc_use_lightmap_bicubic_filter) {
ambient_light += textureArray_bicubic(lightmap_textures[ofs], uvw, lightmaps.data[ofs].light_texture_size).rgb * lightmaps.data[ofs].exposure_normalization;
diff --git a/servers/rendering/rendering_context_driver.cpp b/servers/rendering/rendering_context_driver.cpp
index 23e091e00c..b623be4098 100644
--- a/servers/rendering/rendering_context_driver.cpp
+++ b/servers/rendering/rendering_context_driver.cpp
@@ -84,6 +84,51 @@ void RenderingContextDriver::window_destroy(DisplayServer::WindowID p_window) {
window_surface_map.erase(p_window);
}
+String RenderingContextDriver::get_driver_and_device_memory_report() const {
+ String report;
+
+ const uint32_t num_tracked_obj_types = static_cast<uint32_t>(get_tracked_object_type_count());
+
+ report += "=== Driver Memory Report ===";
+
+ report += "\nLaunch with --extra-gpu-memory-tracking and build with "
+ "DEBUG_ENABLED for this functionality to work.";
+ report += "\nDevice memory may be unavailable if the API does not support it"
+ "(e.g. VK_EXT_device_memory_report is unsupported).";
+ report += "\n";
+
+ report += "\nTotal Driver Memory:";
+ report += String::num_real(double(get_driver_total_memory()) / (1024.0 * 1024.0));
+ report += " MB";
+ report += "\nTotal Driver Num Allocations: ";
+ report += String::num_uint64(get_driver_allocation_count());
+
+ report += "\nTotal Device Memory:";
+ report += String::num_real(double(get_device_total_memory()) / (1024.0 * 1024.0));
+ report += " MB";
+ report += "\nTotal Device Num Allocations: ";
+ report += String::num_uint64(get_device_allocation_count());
+
+ report += "\n\nMemory use by object type (CSV format):";
+ report += "\n\nCategory; Driver memory in MB; Driver Allocation Count; "
+ "Device memory in MB; Device Allocation Count";
+
+ for (uint32_t i = 0u; i < num_tracked_obj_types; ++i) {
+ report += "\n";
+ report += get_tracked_object_name(i);
+ report += ";";
+ report += String::num_real(double(get_driver_memory_by_object_type(i)) / (1024.0 * 1024.0));
+ report += ";";
+ report += String::num_uint64(get_driver_allocs_by_object_type(i));
+ report += ";";
+ report += String::num_real(double(get_device_memory_by_object_type(i)) / (1024.0 * 1024.0));
+ report += ";";
+ report += String::num_uint64(get_device_allocs_by_object_type(i));
+ }
+
+ return report;
+}
+
const char *RenderingContextDriver::get_tracked_object_name(uint32_t p_type_index) const {
return "Tracking Unsupported by API";
}
diff --git a/servers/rendering/rendering_context_driver.h b/servers/rendering/rendering_context_driver.h
index 8449db442c..2e5951ae4f 100644
--- a/servers/rendering/rendering_context_driver.h
+++ b/servers/rendering/rendering_context_driver.h
@@ -102,6 +102,8 @@ public:
virtual void surface_destroy(SurfaceID p_surface) = 0;
virtual bool is_debug_utils_enabled() const = 0;
+ String get_driver_and_device_memory_report() const;
+
virtual const char *get_tracked_object_name(uint32_t p_type_index) const;
virtual uint64_t get_tracked_object_type_count() const;
diff --git a/servers/rendering/rendering_device.cpp b/servers/rendering/rendering_device.cpp
index 332e18bb68..9e3ab5da49 100644
--- a/servers/rendering/rendering_device.cpp
+++ b/servers/rendering/rendering_device.cpp
@@ -5723,6 +5723,10 @@ uint64_t RenderingDevice::get_driver_resource(DriverResource p_resource, RID p_r
return driver->get_resource_native_handle(p_resource, driver_id);
}
+String RenderingDevice::get_driver_and_device_memory_report() const {
+ return context->get_driver_and_device_memory_report();
+}
+
String RenderingDevice::get_tracked_object_name(uint32_t p_type_index) const {
return context->get_tracked_object_name(p_type_index);
}
@@ -6077,6 +6081,7 @@ void RenderingDevice::_bind_methods() {
ClassDB::bind_method(D_METHOD("get_perf_report"), &RenderingDevice::get_perf_report);
+ ClassDB::bind_method(D_METHOD("get_driver_and_device_memory_report"), &RenderingDevice::get_driver_and_device_memory_report);
ClassDB::bind_method(D_METHOD("get_tracked_object_name", "type_index"), &RenderingDevice::get_tracked_object_name);
ClassDB::bind_method(D_METHOD("get_tracked_object_type_count"), &RenderingDevice::get_tracked_object_type_count);
ClassDB::bind_method(D_METHOD("get_driver_total_memory"), &RenderingDevice::get_driver_total_memory);
diff --git a/servers/rendering/rendering_device.h b/servers/rendering/rendering_device.h
index 362fe499e4..d8f9e2c31a 100644
--- a/servers/rendering/rendering_device.h
+++ b/servers/rendering/rendering_device.h
@@ -1417,6 +1417,8 @@ public:
uint64_t get_driver_resource(DriverResource p_resource, RID p_rid = RID(), uint64_t p_index = 0);
+ String get_driver_and_device_memory_report() const;
+
String get_tracked_object_name(uint32_t p_type_index) const;
uint64_t get_tracked_object_type_count() const;
diff --git a/servers/rendering/shader_language.cpp b/servers/rendering/shader_language.cpp
index f8d00ba7ec..8457a09055 100644
--- a/servers/rendering/shader_language.cpp
+++ b/servers/rendering/shader_language.cpp
@@ -9755,6 +9755,11 @@ Error ShaderLanguage::_parse_shader(const HashMap<StringName, FunctionInfo> &p_f
break;
}
+ if (is_constant) {
+ _set_error(vformat(RTR("'%s' qualifier cannot be used with a function return type."), "const"));
+ return ERR_PARSE_ERROR;
+ }
+
FunctionInfo builtins;
if (p_functions.has(name)) {
builtins = p_functions[name];
diff --git a/tests/core/object/test_class_db.h b/tests/core/object/test_class_db.h
index d2d7b6a8b2..8515ba7644 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);
}
}