Merge pull request #51310 from bruvzg/🍎🍏🍐🍎🍎

This commit is contained in:
Rémi Verschelde 2022-01-18 11:51:01 +01:00 committed by GitHub
commit 567b600362
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 4004 additions and 111 deletions

View File

@ -24,10 +24,7 @@
<string>$signature</string> <string>$signature</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>$version</string> <string>$version</string>
<key>NSMicrophoneUsageDescription</key> $usage_descriptions
<string>$microphone_usage_description</string>
<key>NSCameraUsageDescription</key>
<string>$camera_usage_description</string>
<key>NSHumanReadableCopyright</key> <key>NSHumanReadableCopyright</key>
<string>$copyright</string> <string>$copyright</string>
<key>CFBundleSupportedPlatforms</key> <key>CFBundleSupportedPlatforms</key>
@ -46,6 +43,6 @@
<string>10.12</string> <string>10.12</string>
</dict> </dict>
<key>NSHighResolutionCapable</key> <key>NSHighResolutionCapable</key>
$highres $highres
</dict> </dict>
</plist> </plist>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,369 @@
/*************************************************************************/
/* codesign.h */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
/* */
/* 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. */
/*************************************************************************/
// macOS code signature creation utility.
//
// Current implementation has the following limitation:
// - Only version 11.3.0 signatures are supported.
// - Only "framework" and "app" bundle types are supported.
// - Page hash array scattering is not supported.
// - Reading and writing binary property lists i snot supported (third-party frameworks with binary Info.plist will not work unless .plist is converted to text format).
// - Requirements code generator is not implemented (only hard-coded requirements for the ad-hoc signing is supported).
// - RFC5652/CMS blob generation is not implemented, supports ad-hoc signing only.
#ifndef CODESIGN_H
#define CODESIGN_H
#include "core/crypto/crypto.h"
#include "core/crypto/crypto_core.h"
#include "core/io/dir_access.h"
#include "core/io/file_access.h"
#include "core/object/ref_counted.h"
#include "modules/modules_enabled.gen.h" // For regex.
#ifdef MODULE_REGEX_ENABLED
#include "modules/regex/regex.h"
#endif
#include "plist.h"
#ifdef MODULE_REGEX_ENABLED
/*************************************************************************/
/* CodeSignCodeResources */
/*************************************************************************/
class CodeSignCodeResources {
public:
enum class CRMatch {
CR_MATCH_NO,
CR_MATCH_YES,
CR_MATCH_NESTED,
CR_MATCH_OPTIONAL,
};
private:
struct CRFile {
String name;
String hash;
String hash2;
bool optional;
bool nested;
String requirements;
};
struct CRRule {
String file_pattern;
String key;
int weight;
bool store;
CRRule() {
weight = 1;
store = true;
}
CRRule(const String &p_file_pattern, const String &p_key, int p_weight, bool p_store) {
file_pattern = p_file_pattern;
key = p_key;
weight = p_weight;
store = p_store;
}
};
Vector<CRRule> rules1;
Vector<CRRule> rules2;
Vector<CRFile> files1;
Vector<CRFile> files2;
String hash_sha1_base64(const String &p_path);
String hash_sha256_base64(const String &p_path);
public:
void add_rule1(const String &p_rule, const String &p_key = "", int p_weight = 0, bool p_store = true);
void add_rule2(const String &p_rule, const String &p_key = "", int p_weight = 0, bool p_store = true);
CRMatch match_rules1(const String &p_path) const;
CRMatch match_rules2(const String &p_path) const;
bool add_file1(const String &p_root, const String &p_path);
bool add_file2(const String &p_root, const String &p_path);
bool add_nested_file(const String &p_root, const String &p_path, const String &p_exepath);
bool add_folder_recursive(const String &p_root, const String &p_path = "", const String &p_main_exe_path = "");
bool save_to_file(const String &p_path);
};
/*************************************************************************/
/* CodeSignBlob */
/*************************************************************************/
class CodeSignBlob : public RefCounted {
public:
virtual PackedByteArray get_hash_sha1() const = 0;
virtual PackedByteArray get_hash_sha256() const = 0;
virtual int get_size() const = 0;
virtual uint32_t get_index_type() const = 0;
virtual void write_to_file(FileAccess *p_file) const = 0;
};
/*************************************************************************/
/* CodeSignRequirements */
/*************************************************************************/
// Note: Proper code generator is not implemented (any we probably won't ever need it), just a hardcoded bytecode for the limited set of cases.
class CodeSignRequirements : public CodeSignBlob {
PackedByteArray blob;
static inline size_t PAD(size_t s, size_t a) {
return (s % a == 0) ? 0 : (a - s % a);
}
_FORCE_INLINE_ void _parse_certificate_slot(uint32_t &r_pos, String &r_out, uint32_t p_rq_size) const;
_FORCE_INLINE_ void _parse_key(uint32_t &r_pos, String &r_out, uint32_t p_rq_size) const;
_FORCE_INLINE_ void _parse_oid_key(uint32_t &r_pos, String &r_out, uint32_t p_rq_size) const;
_FORCE_INLINE_ void _parse_hash_string(uint32_t &r_pos, String &r_out, uint32_t p_rq_size) const;
_FORCE_INLINE_ void _parse_value(uint32_t &r_pos, String &r_out, uint32_t p_rq_size) const;
_FORCE_INLINE_ void _parse_date(uint32_t &r_pos, String &r_out, uint32_t p_rq_size) const;
_FORCE_INLINE_ bool _parse_match(uint32_t &r_pos, String &r_out, uint32_t p_rq_size) const;
public:
CodeSignRequirements();
CodeSignRequirements(const PackedByteArray &p_data);
Vector<String> parse_requirements() const;
virtual PackedByteArray get_hash_sha1() const override;
virtual PackedByteArray get_hash_sha256() const override;
virtual int get_size() const override;
virtual uint32_t get_index_type() const override { return 0x00000002; };
virtual void write_to_file(FileAccess *p_file) const override;
};
/*************************************************************************/
/* CodeSignEntitlementsText */
/*************************************************************************/
// PList formatted entitlements.
class CodeSignEntitlementsText : public CodeSignBlob {
PackedByteArray blob;
public:
CodeSignEntitlementsText();
CodeSignEntitlementsText(const String &p_string);
virtual PackedByteArray get_hash_sha1() const override;
virtual PackedByteArray get_hash_sha256() const override;
virtual int get_size() const override;
virtual uint32_t get_index_type() const override { return 0x00000005; };
virtual void write_to_file(FileAccess *p_file) const override;
};
/*************************************************************************/
/* CodeSignEntitlementsBinary */
/*************************************************************************/
// ASN.1 serialized entitlements.
class CodeSignEntitlementsBinary : public CodeSignBlob {
PackedByteArray blob;
public:
CodeSignEntitlementsBinary();
CodeSignEntitlementsBinary(const String &p_string);
virtual PackedByteArray get_hash_sha1() const override;
virtual PackedByteArray get_hash_sha256() const override;
virtual int get_size() const override;
virtual uint32_t get_index_type() const override { return 0x00000007; };
virtual void write_to_file(FileAccess *p_file) const override;
};
/*************************************************************************/
/* CodeSignCodeDirectory */
/*************************************************************************/
// Code Directory, runtime options, code segment and special structure hashes.
class CodeSignCodeDirectory : public CodeSignBlob {
public:
enum Slot {
SLOT_INFO_PLIST = -1,
SLOT_REQUIREMENTS = -2,
SLOT_RESOURCES = -3,
SLOT_APP_SPECIFIC = -4, // Unused.
SLOT_ENTITLEMENTS = -5,
SLOT_RESERVER1 = -6, // Unused.
SLOT_DER_ENTITLEMENTS = -7,
};
enum CodeSignExecSegFlags {
EXECSEG_MAIN_BINARY = 0x1,
EXECSEG_ALLOW_UNSIGNED = 0x10,
EXECSEG_DEBUGGER = 0x20,
EXECSEG_JIT = 0x40,
EXECSEG_SKIP_LV = 0x80,
EXECSEG_CAN_LOAD_CDHASH = 0x100,
EXECSEG_CAN_EXEC_CDHASH = 0x200,
};
enum CodeSignatureFlags {
SIGNATURE_HOST = 0x0001,
SIGNATURE_ADHOC = 0x0002,
SIGNATURE_TASK_ALLOW = 0x0004,
SIGNATURE_INSTALLER = 0x0008,
SIGNATURE_FORCED_LV = 0x0010,
SIGNATURE_INVALID_ALLOWED = 0x0020,
SIGNATURE_FORCE_HARD = 0x0100,
SIGNATURE_FORCE_KILL = 0x0200,
SIGNATURE_FORCE_EXPIRATION = 0x0400,
SIGNATURE_RESTRICT = 0x0800,
SIGNATURE_ENFORCEMENT = 0x1000,
SIGNATURE_LIBRARY_VALIDATION = 0x2000,
SIGNATURE_ENTITLEMENTS_VALIDATED = 0x4000,
SIGNATURE_NVRAM_UNRESTRICTED = 0x8000,
SIGNATURE_RUNTIME = 0x10000,
SIGNATURE_LINKER_SIGNED = 0x20000,
};
private:
PackedByteArray blob;
struct CodeDirectoryHeader {
uint32_t version; // Using version 0x0020500.
uint32_t flags; // // Option flags.
uint32_t hash_offset; // Slot zero offset.
uint32_t ident_offset; // Identifier string offset.
uint32_t special_slots; // Nr. of slots with negative index.
uint32_t code_slots; // Nr. of slots with index >= 0, (code_limit / page_size).
uint32_t code_limit; // Everything before code signature load command offset.
uint8_t hash_size; // 20 (SHA-1) or 32 (SHA-256).
uint8_t hash_type; // 1 (SHA-1) or 2 (SHA-256).
uint8_t platform; // Not used.
uint8_t page_size; // Page size, power of two, 2^12 (4096).
uint32_t spare2; // Not used.
// Version 0x20100
uint32_t scatter_vector_offset; // Set to 0 and ignore.
// Version 0x20200
uint32_t team_offset; // Team id string offset.
// Version 0x20300
uint32_t spare3; // Not used.
uint64_t code_limit_64; // Set to 0 and ignore.
// Version 0x20400
uint64_t exec_seg_base; // Start of the signed code segmet.
uint64_t exec_seg_limit; // Code segment (__TEXT) vmsize.
uint64_t exec_seg_flags; // Executable segment flags.
// Version 0x20500
uint32_t runtime; // Runtime version.
uint32_t pre_encrypt_offset; // Set to 0 and ignore.
};
int32_t pages = 0;
int32_t remain = 0;
int32_t code_slots = 0;
int32_t special_slots = 0;
public:
CodeSignCodeDirectory();
CodeSignCodeDirectory(uint8_t p_hash_size, uint8_t p_hash_type, bool p_main, const CharString &p_id, const CharString &p_team_id, uint32_t p_page_size, uint64_t p_exe_limit, uint64_t p_code_limit);
int32_t get_page_count();
int32_t get_page_remainder();
bool set_hash_in_slot(const PackedByteArray &p_hash, int p_slot);
virtual PackedByteArray get_hash_sha1() const override;
virtual PackedByteArray get_hash_sha256() const override;
virtual int get_size() const override;
virtual uint32_t get_index_type() const override { return 0x00000000; };
virtual void write_to_file(FileAccess *p_file) const override;
};
/*************************************************************************/
/* CodeSignSignature */
/*************************************************************************/
class CodeSignSignature : public CodeSignBlob {
PackedByteArray blob;
public:
CodeSignSignature();
virtual PackedByteArray get_hash_sha1() const override;
virtual PackedByteArray get_hash_sha256() const override;
virtual int get_size() const override;
virtual uint32_t get_index_type() const override { return 0x00010000; };
virtual void write_to_file(FileAccess *p_file) const override;
};
/*************************************************************************/
/* CodeSignSuperBlob */
/*************************************************************************/
class CodeSignSuperBlob {
Vector<Ref<CodeSignBlob>> blobs;
public:
bool add_blob(const Ref<CodeSignBlob> &p_blob);
int get_size() const;
void write_to_file(FileAccess *p_file) const;
};
/*************************************************************************/
/* CodeSign */
/*************************************************************************/
class CodeSign {
static PackedByteArray file_hash_sha1(const String &p_path);
static PackedByteArray file_hash_sha256(const String &p_path);
static Error _codesign_file(bool p_use_hardened_runtime, bool p_force, const String &p_info, const String &p_exe_path, const String &p_bundle_path, const String &p_ent_path, bool p_ios_bundle, String &r_error_msg);
public:
static Error codesign(bool p_use_hardened_runtime, bool p_force, const String &p_path, const String &p_ent_path, String &r_error_msg);
};
#endif // MODULE_REGEX_ENABLED
#endif // CODESIGN_H

View File

@ -33,6 +33,9 @@
#include "export_plugin.h" #include "export_plugin.h"
void register_osx_exporter() { void register_osx_exporter() {
EDITOR_DEF("export/macos/force_builtin_codesign", false);
EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::BOOL, "export/macos/force_builtin_codesign", PROPERTY_HINT_NONE));
Ref<EditorExportPlatformOSX> platform; Ref<EditorExportPlatformOSX> platform;
platform.instantiate(); platform.instantiate();

View File

@ -28,6 +28,9 @@
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/*************************************************************************/ /*************************************************************************/
#include "modules/modules_enabled.gen.h" // For regex.
#include "codesign.h"
#include "export_plugin.h" #include "export_plugin.h"
void EditorExportPlatformOSX::get_preset_features(const Ref<EditorExportPreset> &p_preset, List<String> *r_features) { void EditorExportPlatformOSX::get_preset_features(const Ref<EditorExportPreset> &p_preset, List<String> *r_features) {
@ -58,15 +61,25 @@ void EditorExportPlatformOSX::get_export_options(List<ExportOption> *r_options)
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/version"), "1.0")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/version"), "1.0"));
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/copyright"), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/copyright"), ""));
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "display/high_res"), false)); r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "display/high_res"), false));
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/camera_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use the camera"), ""));
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/microphone_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use the microphone"), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/microphone_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use the microphone"), ""));
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/camera_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use the camera"), ""));
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/location_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use the location information"), ""));
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/address_book_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use the address book"), ""));
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/calendar_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use the calendar"), ""));
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/photos_library_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use the photo library"), ""));
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/desktop_folder_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use Desktop folder"), ""));
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/documents_folder_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use Documents folder"), ""));
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/downloads_folder_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use Downloads folder"), ""));
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/network_volumes_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use network volumes"), ""));
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/removable_volumes_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use removable volumes"), ""));
#ifdef OSX_ENABLED
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/enable"), true)); r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/enable"), true));
#ifdef OSX_ENABLED
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "codesign/identity", PROPERTY_HINT_PLACEHOLDER_TEXT, "Type: Name (ID)"), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "codesign/identity", PROPERTY_HINT_PLACEHOLDER_TEXT, "Type: Name (ID)"), ""));
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/timestamp"), true)); r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/timestamp"), true));
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/hardened_runtime"), true)); #endif
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/replace_existing_signature"), true)); r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/replace_existing_signature"), true));
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/hardened_runtime"), true));
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "codesign/entitlements/custom_file", PROPERTY_HINT_GLOBAL_FILE, "*.plist"), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "codesign/entitlements/custom_file", PROPERTY_HINT_GLOBAL_FILE, "*.plist"), ""));
if (!Engine::get_singleton()->has_singleton("GodotSharp")) { if (!Engine::get_singleton()->has_singleton("GodotSharp")) {
@ -97,6 +110,7 @@ void EditorExportPlatformOSX::get_export_options(List<ExportOption> *r_options)
r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "codesign/entitlements/app_sandbox/files_movies", PROPERTY_HINT_ENUM, "No,Read-only,Read-write"), 0)); r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "codesign/entitlements/app_sandbox/files_movies", PROPERTY_HINT_ENUM, "No,Read-only,Read-write"), 0));
r_options->push_back(ExportOption(PropertyInfo(Variant::ARRAY, "codesign/entitlements/app_sandbox/helper_executables", PROPERTY_HINT_ARRAY_TYPE, itos(Variant::STRING) + "/" + itos(PROPERTY_HINT_GLOBAL_FILE) + ":"), Array())); r_options->push_back(ExportOption(PropertyInfo(Variant::ARRAY, "codesign/entitlements/app_sandbox/helper_executables", PROPERTY_HINT_ARRAY_TYPE, itos(Variant::STRING) + "/" + itos(PROPERTY_HINT_GLOBAL_FILE) + ":"), Array()));
#ifdef OSX_ENABLED
r_options->push_back(ExportOption(PropertyInfo(Variant::PACKED_STRING_ARRAY, "codesign/custom_options"), PackedStringArray())); r_options->push_back(ExportOption(PropertyInfo(Variant::PACKED_STRING_ARRAY, "codesign/custom_options"), PackedStringArray()));
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "notarization/enable"), false)); r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "notarization/enable"), false));
@ -305,13 +319,56 @@ void EditorExportPlatformOSX::_fix_plist(const Ref<EditorExportPreset> &p_preset
} else if (lines[i].find("$copyright") != -1) { } else if (lines[i].find("$copyright") != -1) {
strnew += lines[i].replace("$copyright", p_preset->get("application/copyright")) + "\n"; strnew += lines[i].replace("$copyright", p_preset->get("application/copyright")) + "\n";
} else if (lines[i].find("$highres") != -1) { } else if (lines[i].find("$highres") != -1) {
strnew += lines[i].replace("$highres", p_preset->get("display/high_res") ? "<true/>" : "<false/>") + "\n"; strnew += lines[i].replace("$highres", p_preset->get("display/high_res") ? "\t<true/>" : "\t<false/>") + "\n";
} else if (lines[i].find("$camera_usage_description") != -1) { } else if (lines[i].find("$usage_descriptions") != -1) {
String description = p_preset->get("privacy/camera_usage_description"); String descriptions;
strnew += lines[i].replace("$camera_usage_description", description) + "\n"; if (!((String)p_preset->get("privacy/microphone_usage_description")).is_empty()) {
} else if (lines[i].find("$microphone_usage_description") != -1) { descriptions += "\t<key>NSMicrophoneUsageDescription</key>\n";
String description = p_preset->get("privacy/microphone_usage_description"); descriptions += "\t<string>" + (String)p_preset->get("privacy/microphone_usage_description") + "</string>\n";
strnew += lines[i].replace("$microphone_usage_description", description) + "\n"; }
if (!((String)p_preset->get("privacy/camera_usage_description")).is_empty()) {
descriptions += "\t<key>NSCameraUsageDescription</key>\n";
descriptions += "\t<string>" + (String)p_preset->get("privacy/camera_usage_description") + "</string>\n";
}
if (!((String)p_preset->get("privacy/location_usage_description")).is_empty()) {
descriptions += "\t<key>NSLocationUsageDescription</key>\n";
descriptions += "\t<string>" + (String)p_preset->get("privacy/location_usage_description") + "</string>\n";
}
if (!((String)p_preset->get("privacy/address_book_usage_description")).is_empty()) {
descriptions += "\t<key>NSContactsUsageDescription</key>\n";
descriptions += "\t<string>" + (String)p_preset->get("privacy/address_book_usage_description") + "</string>\n";
}
if (!((String)p_preset->get("privacy/calendar_usage_description")).is_empty()) {
descriptions += "\t<key>NSCalendarsUsageDescription</key>\n";
descriptions += "\t<string>" + (String)p_preset->get("privacy/calendar_usage_description") + "</string>\n";
}
if (!((String)p_preset->get("privacy/photos_library_usage_description")).is_empty()) {
descriptions += "\t<key>NSPhotoLibraryUsageDescription</key>\n";
descriptions += "\t<string>" + (String)p_preset->get("privacy/photos_library_usage_description") + "</string>\n";
}
if (!((String)p_preset->get("privacy/desktop_folder_usage_description")).is_empty()) {
descriptions += "\t<key>NSDesktopFolderUsageDescription</key>\n";
descriptions += "\t<string>" + (String)p_preset->get("privacy/desktop_folder_usage_description") + "</string>\n";
}
if (!((String)p_preset->get("privacy/documents_folder_usage_description")).is_empty()) {
descriptions += "\t<key>NSDocumentsFolderUsageDescription</key>\n";
descriptions += "\t<string>" + (String)p_preset->get("privacy/documents_folder_usage_description") + "</string>\n";
}
if (!((String)p_preset->get("privacy/downloads_folder_usage_description")).is_empty()) {
descriptions += "\t<key>NSDownloadsFolderUsageDescription</key>\n";
descriptions += "\t<string>" + (String)p_preset->get("privacy/downloads_folder_usage_description") + "</string>\n";
}
if (!((String)p_preset->get("privacy/network_volumes_usage_description")).is_empty()) {
descriptions += "\t<key>NSNetworkVolumesUsageDescription</key>\n";
descriptions += "\t<string>" + (String)p_preset->get("privacy/network_volumes_usage_description") + "</string>\n";
}
if (!((String)p_preset->get("privacy/removable_volumes_usage_description")).is_empty()) {
descriptions += "\t<key>NSRemovableVolumesUsageDescription</key>\n";
descriptions += "\t<string>" + (String)p_preset->get("privacy/removable_volumes_usage_description") + "</string>\n";
}
if (!descriptions.is_empty()) {
strnew += lines[i].replace("$usage_descriptions", descriptions);
}
} else { } else {
strnew += lines[i] + "\n"; strnew += lines[i] + "\n";
} }
@ -362,14 +419,16 @@ Error EditorExportPlatformOSX::_notarize(const Ref<EditorExportPreset> &p_preset
Error err = OS::get_singleton()->execute("xcrun", args, &str, nullptr, true); Error err = OS::get_singleton()->execute("xcrun", args, &str, nullptr, true);
ERR_FAIL_COND_V(err != OK, err); ERR_FAIL_COND_V(err != OK, err);
print_line("altool (" + p_path + "):\n" + str); print_verbose("altool (" + p_path + "):\n" + str);
if (str.find("RequestUUID") == -1) { if (str.find("RequestUUID") == -1) {
EditorNode::add_io_error("altool: " + str); EditorNode::add_io_error("altool: " + str);
return FAILED; return FAILED;
} else { } else {
print_line("Note: The notarization process generally takes less than an hour. When the process is completed, you'll receive an email."); print_line(TTR("Note: The notarization process generally takes less than an hour. When the process is completed, you'll receive an email."));
print_line(" You can check progress manually by opening a Terminal and running the following command:"); print_line(" " + TTR("You can check progress manually by opening a Terminal and running the following command:"));
print_line(" \"xcrun altool --notarization-history 0 -u <your email> -p <app-specific pwd>\""); print_line(" \"xcrun altool --notarization-history 0 -u <your email> -p <app-specific pwd>\"");
print_line(" " + TTR("Run the following command to staple notarization ticket to the exported application (optional):"));
print_line(" \"xcrun stapler staple <app path>\"");
} }
#endif #endif
@ -378,71 +437,91 @@ Error EditorExportPlatformOSX::_notarize(const Ref<EditorExportPreset> &p_preset
} }
Error EditorExportPlatformOSX::_code_sign(const Ref<EditorExportPreset> &p_preset, const String &p_path, const String &p_ent_path) { Error EditorExportPlatformOSX::_code_sign(const Ref<EditorExportPreset> &p_preset, const String &p_path, const String &p_ent_path) {
#ifdef OSX_ENABLED bool force_builtin_codesign = EditorSettings::get_singleton()->get("export/macos/force_builtin_codesign");
List<String> args;
bool ad_hoc = (p_preset->get("codesign/identity") == "" || p_preset->get("codesign/identity") == "-"); bool ad_hoc = (p_preset->get("codesign/identity") == "" || p_preset->get("codesign/identity") == "-");
if (p_preset->get("codesign/timestamp")) { if ((!FileAccess::exists("/usr/bin/codesign") && !FileAccess::exists("/bin/codesign")) || force_builtin_codesign) {
if (ad_hoc) { print_verbose("using built-in codesign...");
#ifdef MODULE_REGEX_ENABLED
if (p_preset->get("codesign/timestamp")) {
WARN_PRINT("Timestamping is not compatible with ad-hoc signature, and was disabled!"); WARN_PRINT("Timestamping is not compatible with ad-hoc signature, and was disabled!");
} else {
args.push_back("--timestamp");
} }
} if (p_preset->get("codesign/hardened_runtime")) {
if (p_preset->get("codesign/hardened_runtime")) {
if (ad_hoc) {
WARN_PRINT("Hardened Runtime is not compatible with ad-hoc signature, and was disabled!"); WARN_PRINT("Hardened Runtime is not compatible with ad-hoc signature, and was disabled!");
} else {
args.push_back("--options");
args.push_back("runtime");
} }
}
if (p_path.get_extension() != "dmg") { String error_msg;
args.push_back("--entitlements"); Error err = CodeSign::codesign(false, p_preset->get("codesign/replace_existing_signature"), p_path, p_ent_path, error_msg);
args.push_back(p_ent_path); if (err != OK) {
} EditorNode::add_io_error("Built-in CodeSign: " + error_msg);
return FAILED;
PackedStringArray user_args = p_preset->get("codesign/custom_options");
for (int i = 0; i < user_args.size(); i++) {
String user_arg = user_args[i].strip_edges();
if (!user_arg.is_empty()) {
args.push_back(user_arg);
} }
} #else
ERR_FAIL_V_MSG(FAILED, "Built-in CodeSign require regex module");
args.push_back("-s");
if (ad_hoc) {
args.push_back("-");
} else {
args.push_back(p_preset->get("codesign/identity"));
}
args.push_back("-v"); /* provide some more feedback */
if (p_preset->get("codesign/replace_existing_signature")) {
args.push_back("-f");
}
args.push_back(p_path);
String str;
Error err = OS::get_singleton()->execute("codesign", args, &str, nullptr, true);
ERR_FAIL_COND_V(err != OK, err);
print_line("codesign (" + p_path + "):\n" + str);
if (str.find("no identity found") != -1) {
EditorNode::add_io_error("codesign: no identity found");
return FAILED;
}
if ((str.find("unrecognized blob type") != -1) || (str.find("cannot read entitlement data") != -1)) {
EditorNode::add_io_error("codesign: invalid entitlements file");
return FAILED;
}
#endif #endif
return OK;
} else {
print_verbose("using external codesign...");
List<String> args;
if (p_preset->get("codesign/timestamp")) {
if (ad_hoc) {
WARN_PRINT("Timestamping is not compatible with ad-hoc signature, and was disabled!");
} else {
args.push_back("--timestamp");
}
}
if (p_preset->get("codesign/hardened_runtime")) {
if (ad_hoc) {
WARN_PRINT("Hardened Runtime is not compatible with ad-hoc signature, and was disabled!");
} else {
args.push_back("--options");
args.push_back("runtime");
}
}
return OK; if (p_path.get_extension() != "dmg") {
args.push_back("--entitlements");
args.push_back(p_ent_path);
}
PackedStringArray user_args = p_preset->get("codesign/custom_options");
for (int i = 0; i < user_args.size(); i++) {
String user_arg = user_args[i].strip_edges();
if (!user_arg.is_empty()) {
args.push_back(user_arg);
}
}
args.push_back("-s");
if (ad_hoc) {
args.push_back("-");
} else {
args.push_back(p_preset->get("codesign/identity"));
}
args.push_back("-v"); /* provide some more feedback */
if (p_preset->get("codesign/replace_existing_signature")) {
args.push_back("-f");
}
args.push_back(p_path);
String str;
Error err = OS::get_singleton()->execute("codesign", args, &str, nullptr, true);
ERR_FAIL_COND_V(err != OK, err);
print_verbose("codesign (" + p_path + "):\n" + str);
if (str.find("no identity found") != -1) {
EditorNode::add_io_error("CodeSign: " + TTR("No identity found."));
return FAILED;
}
if ((str.find("unrecognized blob type") != -1) || (str.find("cannot read entitlement data") != -1)) {
EditorNode::add_io_error("CodeSign: " + TTR("Invalid entitlements file."));
return FAILED;
}
return OK;
}
} }
Error EditorExportPlatformOSX::_code_sign_directory(const Ref<EditorExportPreset> &p_preset, const String &p_path, Error EditorExportPlatformOSX::_code_sign_directory(const Ref<EditorExportPreset> &p_preset, const String &p_path,
@ -560,12 +639,12 @@ Error EditorExportPlatformOSX::_create_dmg(const String &p_dmg_path, const Strin
Error err = OS::get_singleton()->execute("hdiutil", args, &str, nullptr, true); Error err = OS::get_singleton()->execute("hdiutil", args, &str, nullptr, true);
ERR_FAIL_COND_V(err != OK, err); ERR_FAIL_COND_V(err != OK, err);
print_line("hdiutil returned: " + str); print_verbose("hdiutil returned: " + str);
if (str.find("create failed") != -1) { if (str.find("create failed") != -1) {
if (str.find("File exists") != -1) { if (str.find("File exists") != -1) {
EditorNode::add_io_error("hdiutil: create failed - file exists"); EditorNode::add_io_error("hdiutil: " + TTR("DMG creation failed, file already exists."));
} else { } else {
EditorNode::add_io_error("hdiutil: create failed"); EditorNode::add_io_error("hdiutil: " + TTR("DMG create failed."));
} }
return FAILED; return FAILED;
} }
@ -602,13 +681,13 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p
FileAccess *src_f = nullptr; FileAccess *src_f = nullptr;
zlib_filefunc_def io = zipio_create_io_from_file(&src_f); zlib_filefunc_def io = zipio_create_io_from_file(&src_f);
if (ep.step("Creating app", 0)) { if (ep.step(TTR("Creating app bundle"), 0)) {
return ERR_SKIP; return ERR_SKIP;
} }
unzFile src_pkg_zip = unzOpen2(src_pkg_name.utf8().get_data(), &io); unzFile src_pkg_zip = unzOpen2(src_pkg_name.utf8().get_data(), &io);
if (!src_pkg_zip) { if (!src_pkg_zip) {
EditorNode::add_io_error("Could not find template app to export:\n" + src_pkg_name); EditorNode::add_io_error(TTR("Could not find template app to export:") + "\n" + src_pkg_name);
return ERR_FILE_NOT_FOUND; return ERR_FILE_NOT_FOUND;
} }
@ -627,12 +706,27 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p
pkg_name = OS::get_singleton()->get_safe_dir_name(pkg_name); pkg_name = OS::get_singleton()->get_safe_dir_name(pkg_name);
String export_format = use_dmg() && p_path.ends_with("dmg") ? "dmg" : "zip"; String export_format;
if (use_dmg() && p_path.ends_with("dmg")) {
export_format = "dmg";
} else if (p_path.ends_with("zip")) {
export_format = "zip";
} else if (p_path.ends_with("app")) {
export_format = "app";
} else {
EditorNode::add_io_error("Invalid export format");
return ERR_CANT_CREATE;
}
// Create our application bundle. // Create our application bundle.
String tmp_app_dir_name = pkg_name + ".app"; String tmp_app_dir_name = pkg_name + ".app";
String tmp_app_path_name = EditorPaths::get_singleton()->get_cache_dir().plus_file(tmp_app_dir_name); String tmp_app_path_name;
print_line("Exporting to " + tmp_app_path_name); if (export_format == "app") {
tmp_app_path_name = p_path;
} else {
tmp_app_path_name = EditorPaths::get_singleton()->get_cache_dir().plus_file(tmp_app_dir_name);
}
print_verbose("Exporting to " + tmp_app_path_name);
Error err = OK; Error err = OK;
@ -641,16 +735,22 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p
err = ERR_CANT_CREATE; err = ERR_CANT_CREATE;
} }
if (DirAccess::exists(tmp_app_dir_name)) {
if (tmp_app_dir->change_dir(tmp_app_path_name) == OK) {
tmp_app_dir->erase_contents_recursive();
}
}
Array helpers = p_preset->get("codesign/entitlements/app_sandbox/helper_executables"); Array helpers = p_preset->get("codesign/entitlements/app_sandbox/helper_executables");
// Create our folder structure. // Create our folder structure.
if (err == OK) { if (err == OK) {
print_line("Creating " + tmp_app_path_name + "/Contents/MacOS"); print_verbose("Creating " + tmp_app_path_name + "/Contents/MacOS");
err = tmp_app_dir->make_dir_recursive(tmp_app_path_name + "/Contents/MacOS"); err = tmp_app_dir->make_dir_recursive(tmp_app_path_name + "/Contents/MacOS");
} }
if (err == OK) { if (err == OK) {
print_line("Creating " + tmp_app_path_name + "/Contents/Frameworks"); print_verbose("Creating " + tmp_app_path_name + "/Contents/Frameworks");
err = tmp_app_dir->make_dir_recursive(tmp_app_path_name + "/Contents/Frameworks"); err = tmp_app_dir->make_dir_recursive(tmp_app_path_name + "/Contents/Frameworks");
} }
@ -660,7 +760,7 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p
} }
if (err == OK) { if (err == OK) {
print_line("Creating " + tmp_app_path_name + "/Contents/Resources"); print_verbose("Creating " + tmp_app_path_name + "/Contents/Resources");
err = tmp_app_dir->make_dir_recursive(tmp_app_path_name + "/Contents/Resources"); err = tmp_app_dir->make_dir_recursive(tmp_app_path_name + "/Contents/Resources");
} }
@ -689,6 +789,25 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p
// Write. // Write.
file = file.replace_first("osx_template.app/", ""); file = file.replace_first("osx_template.app/", "");
if (((info.external_fa >> 16L) & 0120000) == 0120000) {
#ifndef UNIX_ENABLED
WARN_PRINT(vformat("Relative symlinks are not supported on this OS, exported project might be broken!"));
#endif
// Handle symlinks in the archive.
file = tmp_app_path_name.plus_file(file);
if (err == OK) {
err = tmp_app_dir->make_dir_recursive(file.get_base_dir());
}
if (err == OK) {
String lnk_data = String::utf8((const char *)data.ptr(), data.size());
err = tmp_app_dir->create_link(lnk_data, file);
print_verbose(vformat("ADDING SYMLINK %s => %s\n", file, lnk_data));
}
ret = unzGoToNextFile(src_pkg_zip);
continue; // next
}
if (file == "Contents/Info.plist") { if (file == "Contents/Info.plist") {
_fix_plist(p_preset, data, pkg_name); _fix_plist(p_preset, data, pkg_name);
} }
@ -752,7 +871,7 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p
dylibs_found.push_back(file); dylibs_found.push_back(file);
} }
print_line("ADDING: " + file + " size: " + itos(data.size())); print_verbose("ADDING: " + file + " size: " + itos(data.size()));
// Write it into our application bundle. // Write it into our application bundle.
file = tmp_app_path_name.plus_file(file); file = tmp_app_path_name.plus_file(file);
@ -782,12 +901,12 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p
unzClose(src_pkg_zip); unzClose(src_pkg_zip);
if (!found_binary) { if (!found_binary) {
ERR_PRINT("Requested template binary '" + binary_to_use + "' not found. It might be missing from your template archive."); ERR_PRINT(vformat("Requested template binary '%s' not found. It might be missing from your template archive.", binary_to_use));
err = ERR_FILE_NOT_FOUND; err = ERR_FILE_NOT_FOUND;
} }
if (err == OK) { if (err == OK) {
if (ep.step("Making PKG", 1)) { if (ep.step(TTR("Making PKG"), 1)) {
return ERR_SKIP; return ERR_SKIP;
} }
@ -965,6 +1084,22 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p
FileAccess::set_unix_permissions(tmp_app_path_name + "/Contents/Helpers/" + hlp_path.get_file(), 0755); FileAccess::set_unix_permissions(tmp_app_path_name + "/Contents/Helpers/" + hlp_path.get_file(), 0755);
} }
} }
bool ad_hoc = true;
if (err == OK) {
#ifdef OSX_ENABLED
String sign_identity = p_preset->get("codesign/identity");
#else
String sign_identity = "-";
#endif
ad_hoc = (sign_identity == "" || sign_identity == "-");
bool lib_validation = p_preset->get("codesign/entitlements/disable_library_validation");
if ((!dylibs_found.is_empty() || !shared_objects.is_empty()) && sign_enabled && ad_hoc && !lib_validation) {
ERR_PRINT("Application with an ad-hoc signature require 'Disable Library Validation' entitlement to load dynamic libraries.");
err = ERR_CANT_CREATE;
}
}
if (err == OK) { if (err == OK) {
DirAccessRef da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); DirAccessRef da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
for (int i = 0; i < shared_objects.size(); i++) { for (int i = 0; i < shared_objects.size(); i++) {
@ -994,31 +1129,31 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p
} }
if (err == OK && sign_enabled) { if (err == OK && sign_enabled) {
if (ep.step("Code signing bundle", 2)) { if (ep.step(TTR("Code signing bundle"), 2)) {
return ERR_SKIP; return ERR_SKIP;
} }
err = _code_sign(p_preset, tmp_app_path_name + "/Contents/MacOS/" + pkg_name, ent_path); err = _code_sign(p_preset, tmp_app_path_name, ent_path);
} }
if (export_format == "dmg") { if (export_format == "dmg") {
// Create a DMG. // Create a DMG.
if (err == OK) { if (err == OK) {
if (ep.step("Making DMG", 3)) { if (ep.step(TTR("Making DMG"), 3)) {
return ERR_SKIP; return ERR_SKIP;
} }
err = _create_dmg(p_path, pkg_name, tmp_app_path_name); err = _create_dmg(p_path, pkg_name, tmp_app_path_name);
} }
// Sign DMG. // Sign DMG.
if (err == OK && sign_enabled) { if (err == OK && sign_enabled && !ad_hoc) {
if (ep.step("Code signing DMG", 3)) { if (ep.step(TTR("Code signing DMG"), 3)) {
return ERR_SKIP; return ERR_SKIP;
} }
err = _code_sign(p_preset, p_path, ent_path); err = _code_sign(p_preset, p_path, ent_path);
} }
} else { } else if (export_format == "zip") {
// Create ZIP. // Create ZIP.
if (err == OK) { if (err == OK) {
if (ep.step("Making ZIP", 3)) { if (ep.step(TTR("Making ZIP"), 3)) {
return ERR_SKIP; return ERR_SKIP;
} }
if (FileAccess::exists(p_path)) { if (FileAccess::exists(p_path)) {
@ -1037,20 +1172,30 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p
bool noto_enabled = p_preset->get("notarization/enable"); bool noto_enabled = p_preset->get("notarization/enable");
if (err == OK && noto_enabled) { if (err == OK && noto_enabled) {
if (ep.step("Sending archive for notarization", 4)) { if (export_format == "app") {
return ERR_SKIP; WARN_PRINT("Notarization require app to be archived first, select DMG or ZIP export format instead.");
} else {
if (ep.step(TTR("Sending archive for notarization"), 4)) {
return ERR_SKIP;
}
err = _notarize(p_preset, p_path);
} }
err = _notarize(p_preset, p_path);
} }
// Clean up temporary entitlements files. // Clean up temporary entitlements files.
DirAccess::remove_file_or_error(hlp_ent_path); DirAccess::remove_file_or_error(hlp_ent_path);
// Clean up temporary .app dir. // Clean up temporary .app dir and generated entitlements.
tmp_app_dir->change_dir(tmp_app_path_name); if ((String)(p_preset->get("codesign/entitlements/custom_file")) == "") {
tmp_app_dir->erase_contents_recursive(); tmp_app_dir->remove(ent_path);
tmp_app_dir->change_dir(".."); }
tmp_app_dir->remove(tmp_app_dir_name); if (export_format != "app") {
if (tmp_app_dir->change_dir(tmp_app_path_name) == OK) {
tmp_app_dir->erase_contents_recursive();
tmp_app_dir->change_dir("..");
tmp_app_dir->remove(tmp_app_dir_name);
}
}
} }
return err; return err;
@ -1152,7 +1297,7 @@ void EditorExportPlatformOSX::_zip_folder_recursive(zipFile &p_zip, const String
FileAccessRef fa = FileAccess::open(dir.plus_file(f), FileAccess::READ); FileAccessRef fa = FileAccess::open(dir.plus_file(f), FileAccess::READ);
if (!fa) { if (!fa) {
ERR_FAIL_MSG("Can't open file to read from path '" + String(dir.plus_file(f)) + "'."); ERR_FAIL_MSG(vformat("Can't open file to read from path \"%s\".", dir.plus_file(f)));
} }
const int bufsize = 16384; const int bufsize = 16384;
uint8_t buf[bufsize]; uint8_t buf[bufsize];
@ -1209,11 +1354,19 @@ bool EditorExportPlatformOSX::can_export(const Ref<EditorExportPreset> &p_preset
valid = false; valid = false;
} }
#ifdef OSX_ENABLED
bool sign_enabled = p_preset->get("codesign/enable"); bool sign_enabled = p_preset->get("codesign/enable");
bool noto_enabled = p_preset->get("notarization/enable"); bool noto_enabled = p_preset->get("notarization/enable");
bool ad_hoc = ((p_preset->get("codesign/identity") == "") || (p_preset->get("codesign/identity") == "-")); bool ad_hoc = ((p_preset->get("codesign/identity") == "") || (p_preset->get("codesign/identity") == "-"));
#ifdef OSX_ENABLED
if (!ad_hoc && (bool)EditorSettings::get_singleton()->get("export/macos/force_builtin_codesign")) {
err += TTR("Warning: Built-in \"codesign\" is selected in the Editor Settings. Code signing is limited to ad-hoc signature only.") + "\n";
}
if (!ad_hoc && !FileAccess::exists("/usr/bin/codesign") && !FileAccess::exists("/bin/codesign")) {
err += TTR("Warning: Xcode command line tools are not installed, using built-in \"codesign\". Code signing is limited to ad-hoc signature only.") + "\n";
}
#endif
if (noto_enabled) { if (noto_enabled) {
if (ad_hoc) { if (ad_hoc) {
err += TTR("Notarization: Notarization with the ad-hoc signature is not supported.") + "\n"; err += TTR("Notarization: Notarization with the ad-hoc signature is not supported.") + "\n";
@ -1240,7 +1393,11 @@ bool EditorExportPlatformOSX::can_export(const Ref<EditorExportPreset> &p_preset
valid = false; valid = false;
} }
} else { } else {
err += TTR("Notarization is disabled. Exported project will be blocked by Gatekeeper, if it's downloaded from an unknown source.") + "\n"; #ifdef OSX_ENABLED
err += TTR("Warning: Notarization is disabled. Exported project will be blocked by Gatekeeper, if it's downloaded from an unknown source.") + "\n";
#else
err += TTR("Warning: Notarization is not supported on this OS. Exported project will be blocked by Gatekeeper, if it's downloaded from an unknown source.") + "\n";
#endif
if (!sign_enabled) { if (!sign_enabled) {
err += TTR("Code signing is disabled. Exported project will not run on Macs with enabled Gatekeeper and Apple Silicon powered Macs.") + "\n"; err += TTR("Code signing is disabled. Exported project will not run on Macs with enabled Gatekeeper and Apple Silicon powered Macs.") + "\n";
} else { } else {
@ -1252,9 +1409,33 @@ bool EditorExportPlatformOSX::can_export(const Ref<EditorExportPreset> &p_preset
} }
} }
} }
#else
err += TTR("macOS code signing and Notarization is not supported on the host OS. Exported project will not run on Macs with enabled Gatekeeper and Apple Silicon powered Macs.") + "\n"; if (sign_enabled) {
#endif if ((bool)p_preset->get("codesign/entitlements/audio_input") && ((String)p_preset->get("privacy/microphone_usage_description")).is_empty()) {
err += TTR("Privacy: Microphone access is enabled, but usage description is not specified.") + "\n";
valid = false;
}
if ((bool)p_preset->get("codesign/entitlements/camera") && ((String)p_preset->get("privacy/camera_usage_description")).is_empty()) {
err += TTR("Privacy: Camera access is enabled, but usage description is not specified.") + "\n";
valid = false;
}
if ((bool)p_preset->get("codesign/entitlements/location") && ((String)p_preset->get("privacy/location_usage_description")).is_empty()) {
err += TTR("Privacy: Location information access is enabled, but usage description is not specified.") + "\n";
valid = false;
}
if ((bool)p_preset->get("codesign/entitlements/address_book") && ((String)p_preset->get("privacy/address_book_usage_description")).is_empty()) {
err += TTR("Privacy: Address book access is enabled, but usage description is not specified.") + "\n";
valid = false;
}
if ((bool)p_preset->get("codesign/entitlements/calendars") && ((String)p_preset->get("privacy/calendar_usage_description")).is_empty()) {
err += TTR("Privacy: Calendar access is enabled, but usage description is not specified.") + "\n";
valid = false;
}
if ((bool)p_preset->get("codesign/entitlements/photos_library") && ((String)p_preset->get("privacy/photos_library_usage_description")).is_empty()) {
err += TTR("Privacy: Photo library access is enabled, but usage description is not specified.") + "\n";
valid = false;
}
}
if (!err.is_empty()) { if (!err.is_empty()) {
r_error = err; r_error = err;

View File

@ -68,13 +68,13 @@ class EditorExportPlatformOSX : public EditorExportPlatform {
Error _create_dmg(const String &p_dmg_path, const String &p_pkg_name, const String &p_app_path_name); Error _create_dmg(const String &p_dmg_path, const String &p_pkg_name, const String &p_app_path_name);
void _zip_folder_recursive(zipFile &p_zip, const String &p_root_path, const String &p_folder, const String &p_pkg_name); void _zip_folder_recursive(zipFile &p_zip, const String &p_root_path, const String &p_folder, const String &p_pkg_name);
#ifdef OSX_ENABLED
bool use_codesign() const { return true; } bool use_codesign() const { return true; }
#ifdef OSX_ENABLED
bool use_dmg() const { return true; } bool use_dmg() const { return true; }
#else #else
bool use_codesign() const { return false; }
bool use_dmg() const { return false; } bool use_dmg() const { return false; }
#endif #endif
bool is_package_name_valid(const String &p_package, String *r_error = nullptr) const { bool is_package_name_valid(const String &p_package, String *r_error = nullptr) const {
String pname = p_package; String pname = p_package;
@ -113,6 +113,7 @@ public:
list.push_back("dmg"); list.push_back("dmg");
} }
list.push_back("zip"); list.push_back("zip");
list.push_back("app");
return list; return list;
} }
virtual Error export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags = 0) override; virtual Error export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags = 0) override;

View File

@ -0,0 +1,243 @@
/*************************************************************************/
/* lipo.cpp */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
/* */
/* 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 "modules/modules_enabled.gen.h" // For regex.
#include "lipo.h"
#ifdef MODULE_REGEX_ENABLED
bool LipO::is_lipo(const String &p_path) {
FileAccessRef fb = FileAccess::open(p_path, FileAccess::READ);
ERR_FAIL_COND_V_MSG(!fb, false, vformat("LipO: Can't open file: \"%s\".", p_path));
uint32_t magic = fb->get_32();
return (magic == 0xbebafeca || magic == 0xcafebabe || magic == 0xbfbafeca || magic == 0xcafebabf);
}
bool LipO::create_file(const String &p_output_path, const PackedStringArray &p_files) {
close();
fa = FileAccess::open(p_output_path, FileAccess::WRITE);
ERR_FAIL_COND_V_MSG(!fa, false, vformat("LipO: Can't open file: \"%s\".", p_output_path));
uint64_t max_size = 0;
for (int i = 0; i < p_files.size(); i++) {
MachO mh;
if (!mh.open_file(p_files[i])) {
ERR_FAIL_V_MSG(false, vformat("LipO: Invalid MachO file: \"%s.\"", p_files[i]));
}
FatArch arch;
arch.cputype = mh.get_cputype();
arch.cpusubtype = mh.get_cpusubtype();
arch.offset = 0;
arch.size = mh.get_size();
arch.align = mh.get_align();
max_size += arch.size;
archs.push_back(arch);
FileAccessRef fb = FileAccess::open(p_files[i], FileAccess::READ);
if (!fb) {
close();
ERR_FAIL_V_MSG(false, vformat("LipO: Can't open file: \"%s.\"", p_files[i]));
}
}
// Write header.
bool is_64 = (max_size >= std::numeric_limits<uint32_t>::max());
if (is_64) {
fa->store_32(0xbfbafeca);
} else {
fa->store_32(0xbebafeca);
}
fa->store_32(BSWAP32(archs.size()));
uint64_t offset = archs.size() * (is_64 ? 32 : 20) + 8;
for (int i = 0; i < archs.size(); i++) {
archs.write[i].offset = offset + PAD(offset, uint64_t(1) << archs[i].align);
if (is_64) {
fa->store_32(BSWAP32(archs[i].cputype));
fa->store_32(BSWAP32(archs[i].cpusubtype));
fa->store_64(BSWAP64(archs[i].offset));
fa->store_64(BSWAP64(archs[i].size));
fa->store_32(BSWAP32(archs[i].align));
fa->store_32(0);
} else {
fa->store_32(BSWAP32(archs[i].cputype));
fa->store_32(BSWAP32(archs[i].cpusubtype));
fa->store_32(BSWAP32(archs[i].offset));
fa->store_32(BSWAP32(archs[i].size));
fa->store_32(BSWAP32(archs[i].align));
}
offset = archs[i].offset + archs[i].size;
}
// Write files and padding.
for (int i = 0; i < archs.size(); i++) {
FileAccessRef fb = FileAccess::open(p_files[i], FileAccess::READ);
if (!fb) {
close();
ERR_FAIL_V_MSG(false, vformat("LipO: Can't open file: \"%s.\"", p_files[i]));
}
uint64_t cur = fa->get_position();
for (uint64_t j = cur; j < archs[i].offset; j++) {
fa->store_8(0);
}
int pages = archs[i].size / 4096;
int remain = archs[i].size % 4096;
unsigned char step[4096];
for (int j = 0; j < pages; j++) {
uint64_t br = fb->get_buffer(step, 4096);
if (br > 0) {
fa->store_buffer(step, br);
}
}
uint64_t br = fb->get_buffer(step, remain);
if (br > 0) {
fa->store_buffer(step, br);
}
fb->close();
}
return true;
}
bool LipO::open_file(const String &p_path) {
close();
fa = FileAccess::open(p_path, FileAccess::READ);
ERR_FAIL_COND_V_MSG(!fa, false, vformat("LipO: Can't open file: \"%s\".", p_path));
uint32_t magic = fa->get_32();
if (magic == 0xbebafeca) {
// 32-bit fat binary, bswap.
uint32_t nfat_arch = BSWAP32(fa->get_32());
for (uint32_t i = 0; i < nfat_arch; i++) {
FatArch arch;
arch.cputype = BSWAP32(fa->get_32());
arch.cpusubtype = BSWAP32(fa->get_32());
arch.offset = BSWAP32(fa->get_32());
arch.size = BSWAP32(fa->get_32());
arch.align = BSWAP32(fa->get_32());
archs.push_back(arch);
}
} else if (magic == 0xcafebabe) {
// 32-bit fat binary.
uint32_t nfat_arch = fa->get_32();
for (uint32_t i = 0; i < nfat_arch; i++) {
FatArch arch;
arch.cputype = fa->get_32();
arch.cpusubtype = fa->get_32();
arch.offset = fa->get_32();
arch.size = fa->get_32();
arch.align = fa->get_32();
archs.push_back(arch);
}
} else if (magic == 0xbfbafeca) {
// 64-bit fat binary, bswap.
uint32_t nfat_arch = BSWAP32(fa->get_32());
for (uint32_t i = 0; i < nfat_arch; i++) {
FatArch arch;
arch.cputype = BSWAP32(fa->get_32());
arch.cpusubtype = BSWAP32(fa->get_32());
arch.offset = BSWAP64(fa->get_64());
arch.size = BSWAP64(fa->get_64());
arch.align = BSWAP32(fa->get_32());
fa->get_32(); // Skip, reserved.
archs.push_back(arch);
}
} else if (magic == 0xcafebabf) {
// 64-bit fat binary.
uint32_t nfat_arch = fa->get_32();
for (uint32_t i = 0; i < nfat_arch; i++) {
FatArch arch;
arch.cputype = fa->get_32();
arch.cpusubtype = fa->get_32();
arch.offset = fa->get_64();
arch.size = fa->get_64();
arch.align = fa->get_32();
fa->get_32(); // Skip, reserved.
archs.push_back(arch);
}
} else {
close();
ERR_FAIL_V_MSG(false, vformat("LipO: Invalid fat binary: \"%s\".", p_path));
}
return true;
}
int LipO::get_arch_count() const {
ERR_FAIL_COND_V_MSG(!fa, 0, "LipO: File not opened.");
return archs.size();
}
bool LipO::extract_arch(int p_index, const String &p_path) {
ERR_FAIL_COND_V_MSG(!fa, false, "LipO: File not opened.");
ERR_FAIL_INDEX_V(p_index, archs.size(), false);
FileAccessRef fb = FileAccess::open(p_path, FileAccess::WRITE);
ERR_FAIL_COND_V_MSG(!fb, false, vformat("LipO: Can't open file: \"%s\".", p_path));
fa->seek(archs[p_index].offset);
int pages = archs[p_index].size / 4096;
int remain = archs[p_index].size % 4096;
unsigned char step[4096];
for (int i = 0; i < pages; i++) {
uint64_t br = fa->get_buffer(step, 4096);
if (br > 0) {
fb->store_buffer(step, br);
}
}
uint64_t br = fa->get_buffer(step, remain);
if (br > 0) {
fb->store_buffer(step, br);
}
fb->close();
return true;
}
void LipO::close() {
if (fa) {
fa->close();
memdelete(fa);
fa = nullptr;
}
archs.clear();
}
LipO::~LipO() {
close();
}
#endif // MODULE_REGEX_ENABLED

View File

@ -0,0 +1,76 @@
/*************************************************************************/
/* lipo.h */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
/* */
/* 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. */
/*************************************************************************/
// Universal / Universal 2 fat binary file creator and extractor.
#ifndef LIPO_H
#define LIPO_H
#include "core/io/file_access.h"
#include "core/object/ref_counted.h"
#include "modules/modules_enabled.gen.h" // For regex.
#include "macho.h"
#ifdef MODULE_REGEX_ENABLED
class LipO : public RefCounted {
struct FatArch {
uint32_t cputype;
uint32_t cpusubtype;
uint64_t offset;
uint64_t size;
uint32_t align;
};
FileAccess *fa = nullptr;
Vector<FatArch> archs;
static inline size_t PAD(size_t s, size_t a) {
return (a - s % a);
}
public:
static bool is_lipo(const String &p_path);
bool create_file(const String &p_output_path, const PackedStringArray &p_files);
bool open_file(const String &p_path);
int get_arch_count() const;
bool extract_arch(int p_index, const String &p_path);
void close();
~LipO();
};
#endif // MODULE_REGEX_ENABLED
#endif // LIPO_H

View File

@ -0,0 +1,556 @@
/*************************************************************************/
/* macho.cpp */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
/* */
/* 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 "modules/modules_enabled.gen.h" // For regex.
#include "macho.h"
#ifdef MODULE_REGEX_ENABLED
uint32_t MachO::seg_align(uint64_t p_vmaddr, uint32_t p_min, uint32_t p_max) {
uint32_t align = p_max;
if (p_vmaddr != 0) {
uint64_t seg_align = 1;
align = 0;
while ((seg_align & p_vmaddr) == 0) {
seg_align = seg_align << 1;
align++;
}
align = CLAMP(align, p_min, p_max);
}
return align;
}
bool MachO::alloc_signature(uint64_t p_size) {
ERR_FAIL_COND_V_MSG(!fa, false, "MachO: File not opened.");
if (signature_offset != 0) {
// Nothing to do, already have signature load command.
return true;
}
if (lc_limit == 0 || lc_limit + 16 > exe_base) {
ERR_FAIL_V_MSG(false, "MachO: Can't allocate signature load command, please use \"codesign_allocate\" utility first.");
} else {
// Add signature load command.
signature_offset = lc_limit;
fa->seek(lc_limit);
LoadCommandHeader lc;
lc.cmd = LC_CODE_SIGNATURE;
lc.cmdsize = 16;
if (swap) {
lc.cmdsize = BSWAP32(lc.cmdsize);
}
fa->store_buffer((const uint8_t *)&lc, sizeof(LoadCommandHeader));
uint32_t lc_offset = fa->get_length() + PAD(fa->get_length(), 16);
uint32_t lc_size = 0;
if (swap) {
lc_offset = BSWAP32(lc_offset);
lc_size = BSWAP32(lc_size);
}
fa->store_32(lc_offset);
fa->store_32(lc_size);
// Write new command number.
fa->seek(0x10);
uint32_t ncmds = fa->get_32();
uint32_t cmdssize = fa->get_32();
if (swap) {
ncmds = BSWAP32(ncmds);
cmdssize = BSWAP32(cmdssize);
}
ncmds += 1;
cmdssize += 16;
if (swap) {
ncmds = BSWAP32(ncmds);
cmdssize = BSWAP32(cmdssize);
}
fa->seek(0x10);
fa->store_32(ncmds);
fa->store_32(cmdssize);
lc_limit = lc_limit + sizeof(LoadCommandHeader) + 8;
return true;
}
}
bool MachO::is_macho(const String &p_path) {
FileAccessRef fb = FileAccess::open(p_path, FileAccess::READ);
ERR_FAIL_COND_V_MSG(!fb, false, vformat("MachO: Can't open file: \"%s\".", p_path));
uint32_t magic = fb->get_32();
return (magic == 0xcefaedfe || magic == 0xfeedface || magic == 0xcffaedfe || magic == 0xfeedfacf);
}
bool MachO::open_file(const String &p_path) {
fa = FileAccess::open(p_path, FileAccess::READ_WRITE);
ERR_FAIL_COND_V_MSG(!fa, false, vformat("MachO: Can't open file: \"%s\".", p_path));
uint32_t magic = fa->get_32();
MachHeader mach_header;
// Read MachO header.
swap = (magic == 0xcffaedfe || magic == 0xcefaedfe);
if (magic == 0xcefaedfe || magic == 0xfeedface) {
// Thin 32-bit binary.
fa->get_buffer((uint8_t *)&mach_header, sizeof(MachHeader));
} else if (magic == 0xcffaedfe || magic == 0xfeedfacf) {
// Thin 64-bit binary.
fa->get_buffer((uint8_t *)&mach_header, sizeof(MachHeader));
fa->get_32(); // Skip extra reserved field.
} else {
ERR_FAIL_V_MSG(false, vformat("MachO: File is not a valid MachO binary: \"%s\".", p_path));
}
if (swap) {
mach_header.ncmds = BSWAP32(mach_header.ncmds);
mach_header.cpusubtype = BSWAP32(mach_header.cpusubtype);
mach_header.cputype = BSWAP32(mach_header.cputype);
}
cpusubtype = mach_header.cpusubtype;
cputype = mach_header.cputype;
align = 0;
exe_base = std::numeric_limits<uint64_t>::max();
exe_limit = 0;
lc_limit = 0;
link_edit_offset = 0;
signature_offset = 0;
// Read load commands.
for (uint32_t i = 0; i < mach_header.ncmds; i++) {
LoadCommandHeader lc;
fa->get_buffer((uint8_t *)&lc, sizeof(LoadCommandHeader));
if (swap) {
lc.cmd = BSWAP32(lc.cmd);
lc.cmdsize = BSWAP32(lc.cmdsize);
}
uint64_t ps = fa->get_position();
switch (lc.cmd) {
case LC_SEGMENT: {
LoadCommandSegment lc_seg;
fa->get_buffer((uint8_t *)&lc_seg, sizeof(LoadCommandSegment));
if (swap) {
lc_seg.nsects = BSWAP32(lc_seg.nsects);
lc_seg.vmaddr = BSWAP32(lc_seg.vmaddr);
lc_seg.vmsize = BSWAP32(lc_seg.vmsize);
}
align = MAX(align, seg_align(lc_seg.vmaddr, 2, 15));
if (String(lc_seg.segname) == "__TEXT") {
exe_limit = MAX(exe_limit, lc_seg.vmsize);
for (uint32_t j = 0; j < lc_seg.nsects; j++) {
Section lc_sect;
fa->get_buffer((uint8_t *)&lc_sect, sizeof(Section));
if (String(lc_sect.sectname) == "__text") {
if (swap) {
exe_base = MIN(exe_base, BSWAP32(lc_sect.offset));
} else {
exe_base = MIN(exe_base, lc_sect.offset);
}
}
if (swap) {
align = MAX(align, BSWAP32(lc_sect.align));
} else {
align = MAX(align, lc_sect.align);
}
}
} else if (String(lc_seg.segname) == "__LINKEDIT") {
link_edit_offset = ps - 8;
}
} break;
case LC_SEGMENT_64: {
LoadCommandSegment64 lc_seg;
fa->get_buffer((uint8_t *)&lc_seg, sizeof(LoadCommandSegment64));
if (swap) {
lc_seg.nsects = BSWAP32(lc_seg.nsects);
lc_seg.vmaddr = BSWAP64(lc_seg.vmaddr);
lc_seg.vmsize = BSWAP64(lc_seg.vmsize);
}
align = MAX(align, seg_align(lc_seg.vmaddr, 3, 15));
if (String(lc_seg.segname) == "__TEXT") {
exe_limit = MAX(exe_limit, lc_seg.vmsize);
for (uint32_t j = 0; j < lc_seg.nsects; j++) {
Section64 lc_sect;
fa->get_buffer((uint8_t *)&lc_sect, sizeof(Section64));
if (String(lc_sect.sectname) == "__text") {
if (swap) {
exe_base = MIN(exe_base, BSWAP32(lc_sect.offset));
} else {
exe_base = MIN(exe_base, lc_sect.offset);
}
if (swap) {
align = MAX(align, BSWAP32(lc_sect.align));
} else {
align = MAX(align, lc_sect.align);
}
}
}
} else if (String(lc_seg.segname) == "__LINKEDIT") {
link_edit_offset = ps - 8;
}
} break;
case LC_CODE_SIGNATURE: {
signature_offset = ps - 8;
} break;
default: {
} break;
}
fa->seek(ps + lc.cmdsize - 8);
lc_limit = ps + lc.cmdsize - 8;
}
if (exe_limit == 0 || lc_limit == 0) {
ERR_FAIL_V_MSG(false, vformat("MachO: No load commands or executable code found: \"%s\".", p_path));
}
return true;
}
uint64_t MachO::get_exe_base() {
ERR_FAIL_COND_V_MSG(!fa, 0, "MachO: File not opened.");
return exe_base;
}
uint64_t MachO::get_exe_limit() {
ERR_FAIL_COND_V_MSG(!fa, 0, "MachO: File not opened.");
return exe_limit;
}
int32_t MachO::get_align() {
ERR_FAIL_COND_V_MSG(!fa, 0, "MachO: File not opened.");
return align;
}
uint32_t MachO::get_cputype() {
ERR_FAIL_COND_V_MSG(!fa, 0, "MachO: File not opened.");
return cputype;
}
uint32_t MachO::get_cpusubtype() {
ERR_FAIL_COND_V_MSG(!fa, 0, "MachO: File not opened.");
return cpusubtype;
}
uint64_t MachO::get_size() {
ERR_FAIL_COND_V_MSG(!fa, 0, "MachO: File not opened.");
return fa->get_length();
}
uint64_t MachO::get_signature_offset() {
ERR_FAIL_COND_V_MSG(!fa, 0, "MachO: File not opened.");
ERR_FAIL_COND_V_MSG(signature_offset == 0, 0, "MachO: No signature load command.");
fa->seek(signature_offset + 8);
if (swap) {
return BSWAP32(fa->get_32());
} else {
return fa->get_32();
}
}
uint64_t MachO::get_code_limit() {
ERR_FAIL_COND_V_MSG(!fa, 0, "MachO: File not opened.");
if (signature_offset == 0) {
return fa->get_length() + PAD(fa->get_length(), 16);
} else {
return get_signature_offset();
}
}
uint64_t MachO::get_signature_size() {
ERR_FAIL_COND_V_MSG(!fa, 0, "MachO: File not opened.");
ERR_FAIL_COND_V_MSG(signature_offset == 0, 0, "MachO: No signature load command.");
fa->seek(signature_offset + 12);
if (swap) {
return BSWAP32(fa->get_32());
} else {
return fa->get_32();
}
}
bool MachO::is_signed() {
ERR_FAIL_COND_V_MSG(!fa, false, "MachO: File not opened.");
if (signature_offset == 0) {
return false;
}
fa->seek(get_signature_offset());
uint32_t magic = BSWAP32(fa->get_32());
if (magic != 0xfade0cc0) {
return false; // No SuperBlob found.
}
fa->get_32(); // Skip size field, unused.
uint32_t count = BSWAP32(fa->get_32());
for (uint32_t i = 0; i < count; i++) {
uint32_t index_type = BSWAP32(fa->get_32());
uint32_t offset = BSWAP32(fa->get_32());
if (index_type == 0x00000000) { // CodeDirectory index type.
fa->seek(get_signature_offset() + offset + 12);
uint32_t flags = BSWAP32(fa->get_32());
if (flags & 0x20000) {
return false; // Found CD, linker-signed.
} else {
return true; // Found CD, not linker-signed.
}
}
}
return false; // No CD found.
}
PackedByteArray MachO::get_cdhash_sha1() {
ERR_FAIL_COND_V_MSG(!fa, PackedByteArray(), "MachO: File not opened.");
if (signature_offset == 0) {
return PackedByteArray();
}
fa->seek(get_signature_offset());
uint32_t magic = BSWAP32(fa->get_32());
if (magic != 0xfade0cc0) {
return PackedByteArray(); // No SuperBlob found.
}
fa->get_32(); // Skip size field, unused.
uint32_t count = BSWAP32(fa->get_32());
for (uint32_t i = 0; i < count; i++) {
fa->get_32(); // Index type, skip.
uint32_t offset = BSWAP32(fa->get_32());
uint64_t pos = fa->get_position();
fa->seek(get_signature_offset() + offset);
uint32_t cdmagic = BSWAP32(fa->get_32());
uint32_t cdsize = BSWAP32(fa->get_32());
if (cdmagic == 0xfade0c02) { // CodeDirectory.
fa->seek(get_signature_offset() + offset + 36);
uint8_t hash_size = fa->get_8();
uint8_t hash_type = fa->get_8();
if (hash_size == 0x14 && hash_type == 0x01) { /* SHA-1 */
PackedByteArray hash;
hash.resize(0x14);
fa->seek(get_signature_offset() + offset);
PackedByteArray blob;
blob.resize(cdsize);
fa->get_buffer(blob.ptrw(), cdsize);
CryptoCore::SHA1Context ctx;
ctx.start();
ctx.update(blob.ptr(), blob.size());
ctx.finish(hash.ptrw());
return hash;
}
}
fa->seek(pos);
}
return PackedByteArray();
}
PackedByteArray MachO::get_cdhash_sha256() {
ERR_FAIL_COND_V_MSG(!fa, PackedByteArray(), "MachO: File not opened.");
if (signature_offset == 0) {
return PackedByteArray();
}
fa->seek(get_signature_offset());
uint32_t magic = BSWAP32(fa->get_32());
if (magic != 0xfade0cc0) {
return PackedByteArray(); // No SuperBlob found.
}
fa->get_32(); // Skip size field, unused.
uint32_t count = BSWAP32(fa->get_32());
for (uint32_t i = 0; i < count; i++) {
fa->get_32(); // Index type, skip.
uint32_t offset = BSWAP32(fa->get_32());
uint64_t pos = fa->get_position();
fa->seek(get_signature_offset() + offset);
uint32_t cdmagic = BSWAP32(fa->get_32());
uint32_t cdsize = BSWAP32(fa->get_32());
if (cdmagic == 0xfade0c02) { // CodeDirectory.
fa->seek(get_signature_offset() + offset + 36);
uint8_t hash_size = fa->get_8();
uint8_t hash_type = fa->get_8();
if (hash_size == 0x20 && hash_type == 0x02) { /* SHA-256 */
PackedByteArray hash;
hash.resize(0x20);
fa->seek(get_signature_offset() + offset);
PackedByteArray blob;
blob.resize(cdsize);
fa->get_buffer(blob.ptrw(), cdsize);
CryptoCore::SHA256Context ctx;
ctx.start();
ctx.update(blob.ptr(), blob.size());
ctx.finish(hash.ptrw());
return hash;
}
}
fa->seek(pos);
}
return PackedByteArray();
}
PackedByteArray MachO::get_requirements() {
ERR_FAIL_COND_V_MSG(!fa, PackedByteArray(), "MachO: File not opened.");
if (signature_offset == 0) {
return PackedByteArray();
}
fa->seek(get_signature_offset());
uint32_t magic = BSWAP32(fa->get_32());
if (magic != 0xfade0cc0) {
return PackedByteArray(); // No SuperBlob found.
}
fa->get_32(); // Skip size field, unused.
uint32_t count = BSWAP32(fa->get_32());
for (uint32_t i = 0; i < count; i++) {
fa->get_32(); // Index type, skip.
uint32_t offset = BSWAP32(fa->get_32());
uint64_t pos = fa->get_position();
fa->seek(get_signature_offset() + offset);
uint32_t rqmagic = BSWAP32(fa->get_32());
uint32_t rqsize = BSWAP32(fa->get_32());
if (rqmagic == 0xfade0c01) { // Requirements.
PackedByteArray blob;
fa->seek(get_signature_offset() + offset);
blob.resize(rqsize);
fa->get_buffer(blob.ptrw(), rqsize);
return blob;
}
fa->seek(pos);
}
return PackedByteArray();
}
const FileAccess *MachO::get_file() const {
return fa;
}
FileAccess *MachO::get_file() {
return fa;
}
bool MachO::set_signature_size(uint64_t p_size) {
ERR_FAIL_COND_V_MSG(!fa, false, "MachO: File not opened.");
// Ensure signature load command exists.
ERR_FAIL_COND_V_MSG(link_edit_offset == 0, false, "MachO: No __LINKEDIT segment found.");
ERR_FAIL_COND_V_MSG(!alloc_signature(p_size), false, "MachO: Can't allocate signature load command.");
// Update signature load command.
uint64_t old_size = get_signature_size();
uint64_t new_size = p_size + PAD(p_size, 16384);
if (new_size <= old_size) {
fa->seek(get_signature_offset());
for (uint64_t i = 0; i < old_size; i++) {
fa->store_8(0x00);
}
return true;
}
fa->seek(signature_offset + 12);
if (swap) {
fa->store_32(BSWAP32(new_size));
} else {
fa->store_32(new_size);
}
uint64_t end = get_signature_offset() + new_size;
// Update "__LINKEDIT" segment.
LoadCommandHeader lc;
fa->seek(link_edit_offset);
fa->get_buffer((uint8_t *)&lc, sizeof(LoadCommandHeader));
if (swap) {
lc.cmd = BSWAP32(lc.cmd);
lc.cmdsize = BSWAP32(lc.cmdsize);
}
switch (lc.cmd) {
case LC_SEGMENT: {
LoadCommandSegment lc_seg;
fa->get_buffer((uint8_t *)&lc_seg, sizeof(LoadCommandSegment));
if (swap) {
lc_seg.vmsize = BSWAP32(lc_seg.vmsize);
lc_seg.filesize = BSWAP32(lc_seg.filesize);
lc_seg.fileoff = BSWAP32(lc_seg.fileoff);
}
lc_seg.vmsize = end - lc_seg.fileoff;
lc_seg.vmsize += PAD(lc_seg.vmsize, 4096);
lc_seg.filesize = end - lc_seg.fileoff;
if (swap) {
lc_seg.vmsize = BSWAP32(lc_seg.vmsize);
lc_seg.filesize = BSWAP32(lc_seg.filesize);
}
fa->seek(link_edit_offset + 8);
fa->store_buffer((const uint8_t *)&lc_seg, sizeof(LoadCommandSegment));
} break;
case LC_SEGMENT_64: {
LoadCommandSegment64 lc_seg;
fa->get_buffer((uint8_t *)&lc_seg, sizeof(LoadCommandSegment64));
if (swap) {
lc_seg.vmsize = BSWAP64(lc_seg.vmsize);
lc_seg.filesize = BSWAP64(lc_seg.filesize);
lc_seg.fileoff = BSWAP64(lc_seg.fileoff);
}
lc_seg.vmsize = end - lc_seg.fileoff;
lc_seg.vmsize += PAD(lc_seg.vmsize, 4096);
lc_seg.filesize = end - lc_seg.fileoff;
if (swap) {
lc_seg.vmsize = BSWAP64(lc_seg.vmsize);
lc_seg.filesize = BSWAP64(lc_seg.filesize);
}
fa->seek(link_edit_offset + 8);
fa->store_buffer((const uint8_t *)&lc_seg, sizeof(LoadCommandSegment64));
} break;
default: {
ERR_FAIL_V_MSG(false, "MachO: Invalid __LINKEDIT segment type.");
} break;
}
fa->seek(get_signature_offset());
for (uint64_t i = 0; i < new_size; i++) {
fa->store_8(0x00);
}
return true;
}
MachO::~MachO() {
if (fa) {
fa->close();
memdelete(fa);
fa = nullptr;
}
}
#endif // MODULE_REGEX_ENABLED

217
platform/osx/export/macho.h Normal file
View File

@ -0,0 +1,217 @@
/*************************************************************************/
/* macho.h */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
/* */
/* 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. */
/*************************************************************************/
// Mach-O binary object file format parser and editor.
#ifndef MACHO_H
#define MACHO_H
#include "core/crypto/crypto.h"
#include "core/crypto/crypto_core.h"
#include "core/io/file_access.h"
#include "core/object/ref_counted.h"
#include "modules/modules_enabled.gen.h" // For regex.
#ifdef MODULE_REGEX_ENABLED
class MachO : public RefCounted {
struct MachHeader {
uint32_t cputype;
uint32_t cpusubtype;
uint32_t filetype;
uint32_t ncmds;
uint32_t sizeofcmds;
uint32_t flags;
};
enum LoadCommandID {
LC_SEGMENT = 0x00000001,
LC_SYMTAB = 0x00000002,
LC_SYMSEG = 0x00000003,
LC_THREAD = 0x00000004,
LC_UNIXTHREAD = 0x00000005,
LC_LOADFVMLIB = 0x00000006,
LC_IDFVMLIB = 0x00000007,
LC_IDENT = 0x00000008,
LC_FVMFILE = 0x00000009,
LC_PREPAGE = 0x0000000a,
LC_DYSYMTAB = 0x0000000b,
LC_LOAD_DYLIB = 0x0000000c,
LC_ID_DYLIB = 0x0000000d,
LC_LOAD_DYLINKER = 0x0000000e,
LC_ID_DYLINKER = 0x0000000f,
LC_PREBOUND_DYLIB = 0x00000010,
LC_ROUTINES = 0x00000011,
LC_SUB_FRAMEWORK = 0x00000012,
LC_SUB_UMBRELLA = 0x00000013,
LC_SUB_CLIENT = 0x00000014,
LC_SUB_LIBRARY = 0x00000015,
LC_TWOLEVEL_HINTS = 0x00000016,
LC_PREBIND_CKSUM = 0x00000017,
LC_LOAD_WEAK_DYLIB = 0x80000018,
LC_SEGMENT_64 = 0x00000019,
LC_ROUTINES_64 = 0x0000001a,
LC_UUID = 0x0000001b,
LC_RPATH = 0x8000001c,
LC_CODE_SIGNATURE = 0x0000001d,
LC_SEGMENT_SPLIT_INFO = 0x0000001e,
LC_REEXPORT_DYLIB = 0x8000001f,
LC_LAZY_LOAD_DYLIB = 0x00000020,
LC_ENCRYPTION_INFO = 0x00000021,
LC_DYLD_INFO = 0x00000022,
LC_DYLD_INFO_ONLY = 0x80000022,
LC_LOAD_UPWARD_DYLIB = 0x80000023,
LC_VERSION_MIN_MACOSX = 0x00000024,
LC_VERSION_MIN_IPHONEOS = 0x00000025,
LC_FUNCTION_STARTS = 0x00000026,
LC_DYLD_ENVIRONMENT = 0x00000027,
LC_MAIN = 0x80000028,
LC_DATA_IN_CODE = 0x00000029,
LC_SOURCE_VERSION = 0x0000002a,
LC_DYLIB_CODE_SIGN_DRS = 0x0000002b,
LC_ENCRYPTION_INFO_64 = 0x0000002c,
LC_LINKER_OPTION = 0x0000002d,
LC_LINKER_OPTIMIZATION_HINT = 0x0000002e,
LC_VERSION_MIN_TVOS = 0x0000002f,
LC_VERSION_MIN_WATCHOS = 0x00000030,
};
struct LoadCommandHeader {
uint32_t cmd;
uint32_t cmdsize;
};
struct LoadCommandSegment {
char segname[16];
uint32_t vmaddr;
uint32_t vmsize;
uint32_t fileoff;
uint32_t filesize;
uint32_t maxprot;
uint32_t initprot;
uint32_t nsects;
uint32_t flags;
};
struct LoadCommandSegment64 {
char segname[16];
uint64_t vmaddr;
uint64_t vmsize;
uint64_t fileoff;
uint64_t filesize;
uint32_t maxprot;
uint32_t initprot;
uint32_t nsects;
uint32_t flags;
};
struct Section {
char sectname[16];
char segname[16];
uint32_t addr;
uint32_t size;
uint32_t offset;
uint32_t align;
uint32_t reloff;
uint32_t nreloc;
uint32_t flags;
uint32_t reserved1;
uint32_t reserved2;
};
struct Section64 {
char sectname[16];
char segname[16];
uint64_t addr;
uint64_t size;
uint32_t offset;
uint32_t align;
uint32_t reloff;
uint32_t nreloc;
uint32_t flags;
uint32_t reserved1;
uint32_t reserved2;
uint32_t reserved3;
};
FileAccess *fa = nullptr;
bool swap = false;
uint64_t lc_limit = 0;
uint64_t exe_limit = 0;
uint64_t exe_base = std::numeric_limits<uint64_t>::max(); // Start of first __text section.
uint32_t align = 0;
uint32_t cputype = 0;
uint32_t cpusubtype = 0;
uint64_t link_edit_offset = 0; // __LINKEDIT segment offset.
uint64_t signature_offset = 0; // Load command offset.
uint32_t seg_align(uint64_t p_vmaddr, uint32_t p_min, uint32_t p_max);
bool alloc_signature(uint64_t p_size);
static inline size_t PAD(size_t s, size_t a) {
return (a - s % a);
}
public:
static bool is_macho(const String &p_path);
bool open_file(const String &p_path);
uint64_t get_exe_base();
uint64_t get_exe_limit();
int32_t get_align();
uint32_t get_cputype();
uint32_t get_cpusubtype();
uint64_t get_size();
uint64_t get_code_limit();
uint64_t get_signature_offset();
bool is_signed();
PackedByteArray get_cdhash_sha1();
PackedByteArray get_cdhash_sha256();
PackedByteArray get_requirements();
const FileAccess *get_file() const;
FileAccess *get_file();
uint64_t get_signature_size();
bool set_signature_size(uint64_t p_size);
~MachO();
};
#endif // MODULE_REGEX_ENABLED
#endif // MACHO_H

View File

@ -0,0 +1,570 @@
/*************************************************************************/
/* plist.cpp */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
/* */
/* 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 "modules/modules_enabled.gen.h" // For regex.
#include "plist.h"
#ifdef MODULE_REGEX_ENABLED
Ref<PListNode> PListNode::new_array() {
Ref<PListNode> node = memnew(PListNode());
ERR_FAIL_COND_V(node.is_null(), Ref<PListNode>());
node->data_type = PList::PLNodeType::PL_NODE_TYPE_ARRAY;
return node;
}
Ref<PListNode> PListNode::new_dict() {
Ref<PListNode> node = memnew(PListNode());
ERR_FAIL_COND_V(node.is_null(), Ref<PListNode>());
node->data_type = PList::PLNodeType::PL_NODE_TYPE_DICT;
return node;
}
Ref<PListNode> PListNode::new_string(const String &p_string) {
Ref<PListNode> node = memnew(PListNode());
ERR_FAIL_COND_V(node.is_null(), Ref<PListNode>());
node->data_type = PList::PLNodeType::PL_NODE_TYPE_STRING;
node->data_string = p_string.utf8();
return node;
}
Ref<PListNode> PListNode::new_data(const String &p_string) {
Ref<PListNode> node = memnew(PListNode());
ERR_FAIL_COND_V(node.is_null(), Ref<PListNode>());
node->data_type = PList::PLNodeType::PL_NODE_TYPE_DATA;
node->data_string = p_string.utf8();
return node;
}
Ref<PListNode> PListNode::new_date(const String &p_string) {
Ref<PListNode> node = memnew(PListNode());
ERR_FAIL_COND_V(node.is_null(), Ref<PListNode>());
node->data_type = PList::PLNodeType::PL_NODE_TYPE_DATE;
node->data_string = p_string.utf8();
return node;
}
Ref<PListNode> PListNode::new_bool(bool p_bool) {
Ref<PListNode> node = memnew(PListNode());
ERR_FAIL_COND_V(node.is_null(), Ref<PListNode>());
node->data_type = PList::PLNodeType::PL_NODE_TYPE_BOOLEAN;
node->data_bool = p_bool;
return node;
}
Ref<PListNode> PListNode::new_int(int32_t p_int) {
Ref<PListNode> node = memnew(PListNode());
ERR_FAIL_COND_V(node.is_null(), Ref<PListNode>());
node->data_type = PList::PLNodeType::PL_NODE_TYPE_INTEGER;
node->data_int = p_int;
return node;
}
Ref<PListNode> PListNode::new_real(float p_real) {
Ref<PListNode> node = memnew(PListNode());
ERR_FAIL_COND_V(node.is_null(), Ref<PListNode>());
node->data_type = PList::PLNodeType::PL_NODE_TYPE_REAL;
node->data_real = p_real;
return node;
}
bool PListNode::push_subnode(const Ref<PListNode> &p_node, const String &p_key) {
ERR_FAIL_COND_V(p_node.is_null(), false);
if (data_type == PList::PLNodeType::PL_NODE_TYPE_DICT) {
ERR_FAIL_COND_V(p_key.is_empty(), false);
ERR_FAIL_COND_V(data_dict.has(p_key), false);
data_dict[p_key] = p_node;
return true;
} else if (data_type == PList::PLNodeType::PL_NODE_TYPE_ARRAY) {
data_array.push_back(p_node);
return true;
} else {
ERR_FAIL_V_MSG(false, "PList: Invalid parent node type, should be DICT or ARRAY.");
}
}
size_t PListNode::get_asn1_size(uint8_t p_len_octets) const {
// Get size of all data, excluding type and size information.
switch (data_type) {
case PList::PLNodeType::PL_NODE_TYPE_NIL: {
return 0;
} break;
case PList::PLNodeType::PL_NODE_TYPE_DATA:
case PList::PLNodeType::PL_NODE_TYPE_DATE: {
ERR_FAIL_V_MSG(0, "PList: DATE and DATA nodes are not supported by ASN.1 serialization.");
} break;
case PList::PLNodeType::PL_NODE_TYPE_STRING: {
return data_string.length();
} break;
case PList::PLNodeType::PL_NODE_TYPE_BOOLEAN: {
return 1;
} break;
case PList::PLNodeType::PL_NODE_TYPE_INTEGER:
case PList::PLNodeType::PL_NODE_TYPE_REAL: {
return 4;
} break;
case PList::PLNodeType::PL_NODE_TYPE_ARRAY: {
size_t size = 0;
for (int i = 0; i < data_array.size(); i++) {
size += 1 + _asn1_size_len(p_len_octets) + data_array[i]->get_asn1_size(p_len_octets);
}
return size;
} break;
case PList::PLNodeType::PL_NODE_TYPE_DICT: {
size_t size = 0;
for (const Map<String, Ref<PListNode>>::Element *it = data_dict.front(); it; it = it->next()) {
size += 1 + _asn1_size_len(p_len_octets); // Sequence.
size += 1 + _asn1_size_len(p_len_octets) + it->key().utf8().length(); //Key.
size += 1 + _asn1_size_len(p_len_octets) + it->value()->get_asn1_size(p_len_octets); // Value.
}
return size;
} break;
default: {
return 0;
} break;
}
}
int PListNode::_asn1_size_len(uint8_t p_len_octets) {
if (p_len_octets > 1) {
return p_len_octets + 1;
} else {
return 1;
}
}
void PListNode::store_asn1_size(PackedByteArray &p_stream, uint8_t p_len_octets) const {
uint32_t size = get_asn1_size(p_len_octets);
if (p_len_octets > 1) {
p_stream.push_back(0x80 + p_len_octets);
}
for (int i = p_len_octets - 1; i >= 0; i--) {
uint8_t x = (size >> i * 8) & 0xFF;
p_stream.push_back(x);
}
}
bool PListNode::store_asn1(PackedByteArray &p_stream, uint8_t p_len_octets) const {
// Convert to binary ASN1 stream.
bool valid = true;
switch (data_type) {
case PList::PLNodeType::PL_NODE_TYPE_NIL: {
// Nothing to store.
} break;
case PList::PLNodeType::PL_NODE_TYPE_DATE:
case PList::PLNodeType::PL_NODE_TYPE_DATA: {
ERR_FAIL_V_MSG(false, "PList: DATE and DATA nodes are not supported by ASN.1 serialization.");
} break;
case PList::PLNodeType::PL_NODE_TYPE_STRING: {
p_stream.push_back(0x0C);
store_asn1_size(p_stream, p_len_octets);
for (int i = 0; i < data_string.size(); i++) {
p_stream.push_back(data_string[i]);
}
} break;
case PList::PLNodeType::PL_NODE_TYPE_BOOLEAN: {
p_stream.push_back(0x01);
store_asn1_size(p_stream, p_len_octets);
if (data_bool) {
p_stream.push_back(0x01);
} else {
p_stream.push_back(0x00);
}
} break;
case PList::PLNodeType::PL_NODE_TYPE_INTEGER: {
p_stream.push_back(0x02);
store_asn1_size(p_stream, p_len_octets);
for (int i = 4; i >= 0; i--) {
uint8_t x = (data_int >> i * 8) & 0xFF;
p_stream.push_back(x);
}
} break;
case PList::PLNodeType::PL_NODE_TYPE_REAL: {
p_stream.push_back(0x03);
store_asn1_size(p_stream, p_len_octets);
for (int i = 4; i >= 0; i--) {
uint8_t x = (data_int >> i * 8) & 0xFF;
p_stream.push_back(x);
}
} break;
case PList::PLNodeType::PL_NODE_TYPE_ARRAY: {
p_stream.push_back(0x30); // Sequence.
store_asn1_size(p_stream, p_len_octets);
for (int i = 0; i < data_array.size(); i++) {
valid = valid && data_array[i]->store_asn1(p_stream, p_len_octets);
}
} break;
case PList::PLNodeType::PL_NODE_TYPE_DICT: {
p_stream.push_back(0x31); // Set.
store_asn1_size(p_stream, p_len_octets);
for (const Map<String, Ref<PListNode>>::Element *it = data_dict.front(); it; it = it->next()) {
CharString cs = it->key().utf8();
uint32_t size = cs.length();
// Sequence.
p_stream.push_back(0x30);
uint32_t seq_size = 2 * (1 + _asn1_size_len(p_len_octets)) + size + it->value()->get_asn1_size(p_len_octets);
if (p_len_octets > 1) {
p_stream.push_back(0x80 + p_len_octets);
}
for (int i = p_len_octets - 1; i >= 0; i--) {
uint8_t x = (seq_size >> i * 8) & 0xFF;
p_stream.push_back(x);
}
// Key.
p_stream.push_back(0x0C);
if (p_len_octets > 1) {
p_stream.push_back(0x80 + p_len_octets);
}
for (int i = p_len_octets - 1; i >= 0; i--) {
uint8_t x = (size >> i * 8) & 0xFF;
p_stream.push_back(x);
}
for (uint32_t i = 0; i < size; i++) {
p_stream.push_back(cs[i]);
}
// Value.
valid = valid && it->value()->store_asn1(p_stream, p_len_octets);
}
} break;
}
return valid;
}
void PListNode::store_text(String &p_stream, uint8_t p_indent) const {
// Convert to text XML stream.
switch (data_type) {
case PList::PLNodeType::PL_NODE_TYPE_NIL: {
// Nothing to store.
} break;
case PList::PLNodeType::PL_NODE_TYPE_DATA: {
p_stream += String("\t").repeat(p_indent);
p_stream += "<data>\n";
p_stream += String("\t").repeat(p_indent);
p_stream += data_string + "\n";
p_stream += String("\t").repeat(p_indent);
p_stream += "</data>\n";
} break;
case PList::PLNodeType::PL_NODE_TYPE_DATE: {
p_stream += String("\t").repeat(p_indent);
p_stream += "<date>";
p_stream += data_string;
p_stream += "</date>\n";
} break;
case PList::PLNodeType::PL_NODE_TYPE_STRING: {
p_stream += String("\t").repeat(p_indent);
p_stream += "<string>";
p_stream += String::utf8(data_string);
p_stream += "</string>\n";
} break;
case PList::PLNodeType::PL_NODE_TYPE_BOOLEAN: {
p_stream += String("\t").repeat(p_indent);
if (data_bool) {
p_stream += "<true/>\n";
} else {
p_stream += "<false/>\n";
}
} break;
case PList::PLNodeType::PL_NODE_TYPE_INTEGER: {
p_stream += String("\t").repeat(p_indent);
p_stream += "<integer>";
p_stream += itos(data_int);
p_stream += "</integer>\n";
} break;
case PList::PLNodeType::PL_NODE_TYPE_REAL: {
p_stream += String("\t").repeat(p_indent);
p_stream += "<real>";
p_stream += rtos(data_real);
p_stream += "</real>\n";
} break;
case PList::PLNodeType::PL_NODE_TYPE_ARRAY: {
p_stream += String("\t").repeat(p_indent);
p_stream += "<array>\n";
for (int i = 0; i < data_array.size(); i++) {
data_array[i]->store_text(p_stream, p_indent + 1);
}
p_stream += String("\t").repeat(p_indent);
p_stream += "</array>\n";
} break;
case PList::PLNodeType::PL_NODE_TYPE_DICT: {
p_stream += String("\t").repeat(p_indent);
p_stream += "<dict>\n";
for (const Map<String, Ref<PListNode>>::Element *it = data_dict.front(); it; it = it->next()) {
p_stream += String("\t").repeat(p_indent + 1);
p_stream += "<key>";
p_stream += it->key();
p_stream += "</key>\n";
it->value()->store_text(p_stream, p_indent + 1);
}
p_stream += String("\t").repeat(p_indent);
p_stream += "</dict>\n";
} break;
}
}
/*************************************************************************/
PList::PList() {
root = PListNode::new_dict();
}
PList::PList(const String &p_string) {
load_string(p_string);
}
bool PList::load_file(const String &p_filename) {
root = Ref<PListNode>();
FileAccessRef fb = FileAccess::open(p_filename, FileAccess::READ);
if (!fb) {
return false;
}
unsigned char magic[8];
fb->get_buffer(magic, 8);
if (String((const char *)magic, 8) == "bplist00") {
ERR_FAIL_V_MSG(false, "PList: Binary property lists are not supported.");
} else {
// Load text plist.
Error err;
Vector<uint8_t> array = FileAccess::get_file_as_array(p_filename, &err);
ERR_FAIL_COND_V(err != OK, false);
String ret;
ret.parse_utf8((const char *)array.ptr(), array.size());
return load_string(ret);
}
}
bool PList::load_string(const String &p_string) {
root = Ref<PListNode>();
int pos = 0;
bool in_plist = false;
bool done_plist = false;
List<Ref<PListNode>> stack;
String key;
while (pos >= 0) {
int open_token_s = p_string.find("<", pos);
if (open_token_s == -1) {
ERR_FAIL_V_MSG(false, "PList: Unexpected end of data. No tags found.");
}
int open_token_e = p_string.find(">", open_token_s);
pos = open_token_e;
String token = p_string.substr(open_token_s + 1, open_token_e - open_token_s - 1);
if (token.is_empty()) {
ERR_FAIL_V_MSG(false, "PList: Invalid token name.");
}
String value;
if (token[0] == '?' || token[0] == '!') { // Skip <?xml ... ?> and <!DOCTYPE ... >
int end_token_e = p_string.find(">", open_token_s);
pos = end_token_e;
continue;
}
if (token.find("plist", 0) == 0) {
in_plist = true;
continue;
}
if (token == "/plist") {
in_plist = false;
done_plist = true;
break;
}
if (!in_plist) {
ERR_FAIL_V_MSG(false, "PList: Node outside of <plist> tag.");
}
if (token == "dict") {
if (!stack.is_empty()) {
// Add subnode end enter it.
Ref<PListNode> dict = PListNode::new_dict();
dict->data_type = PList::PLNodeType::PL_NODE_TYPE_DICT;
if (!stack.back()->get()->push_subnode(dict, key)) {
ERR_FAIL_V_MSG(false, "PList: Can't push subnode, invalid parent type.");
}
stack.push_back(dict);
} else {
// Add root node.
if (!root.is_null()) {
ERR_FAIL_V_MSG(false, "PList: Root node already set.");
}
Ref<PListNode> dict = PListNode::new_dict();
stack.push_back(dict);
root = dict;
}
continue;
}
if (token == "/dict") {
// Exit current dict.
if (stack.is_empty() || stack.back()->get()->data_type != PList::PLNodeType::PL_NODE_TYPE_DICT) {
ERR_FAIL_V_MSG(false, "PList: Mismatched </dict> tag.");
}
stack.pop_back();
continue;
}
if (token == "array") {
if (!stack.is_empty()) {
// Add subnode end enter it.
Ref<PListNode> arr = PListNode::new_array();
if (!stack.back()->get()->push_subnode(arr, key)) {
ERR_FAIL_V_MSG(false, "PList: Can't push subnode, invalid parent type.");
}
stack.push_back(arr);
} else {
// Add root node.
if (!root.is_null()) {
ERR_FAIL_V_MSG(false, "PList: Root node already set.");
}
Ref<PListNode> arr = PListNode::new_array();
stack.push_back(arr);
root = arr;
}
continue;
}
if (token == "/array") {
// Exit current array.
if (stack.is_empty() || stack.back()->get()->data_type != PList::PLNodeType::PL_NODE_TYPE_ARRAY) {
ERR_FAIL_V_MSG(false, "PList: Mismatched </array> tag.");
}
stack.pop_back();
continue;
}
if (token[token.length() - 1] == '/') {
token = token.substr(0, token.length() - 1);
} else {
int end_token_s = p_string.find("</", pos);
if (end_token_s == -1) {
ERR_FAIL_V_MSG(false, vformat("PList: Mismatched <%s> tag.", token));
}
int end_token_e = p_string.find(">", end_token_s);
pos = end_token_e;
String end_token = p_string.substr(end_token_s + 2, end_token_e - end_token_s - 2);
if (end_token != token) {
ERR_FAIL_V_MSG(false, vformat("PList: Mismatched <%s> and <%s> token pair.", token, end_token));
}
value = p_string.substr(open_token_e + 1, end_token_s - open_token_e - 1);
}
if (token == "key") {
key = value;
} else {
Ref<PListNode> var = nullptr;
if (token == "true") {
var = PListNode::new_bool(true);
} else if (token == "false") {
var = PListNode::new_bool(false);
} else if (token == "integer") {
var = PListNode::new_int(value.to_int());
} else if (token == "real") {
var = PListNode::new_real(value.to_float());
} else if (token == "string") {
var = PListNode::new_string(value);
} else if (token == "data") {
var = PListNode::new_data(value);
} else if (token == "date") {
var = PListNode::new_date(value);
} else {
ERR_FAIL_V_MSG(false, "PList: Invalid value type.");
}
if (stack.is_empty() || !stack.back()->get()->push_subnode(var, key)) {
ERR_FAIL_V_MSG(false, "PList: Can't push subnode, invalid parent type.");
}
}
}
if (!stack.is_empty() || !done_plist) {
ERR_FAIL_V_MSG(false, "PList: Unexpected end of data. Root node is not closed.");
}
return true;
}
PackedByteArray PList::save_asn1() const {
if (root == nullptr) {
ERR_FAIL_V_MSG(PackedByteArray(), "PList: Invalid PList, no root node.");
}
size_t size = root->get_asn1_size(1);
uint8_t len_octets = 0;
if (size < 0x80) {
len_octets = 1;
} else {
size = root->get_asn1_size(2);
if (size < 0xFFFF) {
len_octets = 2;
} else {
size = root->get_asn1_size(3);
if (size < 0xFFFFFF) {
len_octets = 3;
} else {
size = root->get_asn1_size(4);
if (size < 0xFFFFFFFF) {
len_octets = 4;
} else {
ERR_FAIL_V_MSG(PackedByteArray(), "PList: Data is too big for ASN.1 serializer, should be < 4 GiB.");
}
}
}
}
PackedByteArray ret;
if (!root->store_asn1(ret, len_octets)) {
ERR_FAIL_V_MSG(PackedByteArray(), "PList: ASN.1 serializer error.");
}
return ret;
}
String PList::save_text() const {
if (root == nullptr) {
ERR_FAIL_V_MSG(String(), "PList: Invalid PList, no root node.");
}
String ret;
ret += "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
ret += "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n";
ret += "<plist version=\"1.0\">\n";
root->store_text(ret, 0);
ret += "</plist>\n\n";
return ret;
}
Ref<PListNode> PList::get_root() {
return root;
}
#endif // MODULE_REGEX_ENABLED

116
platform/osx/export/plist.h Normal file
View File

@ -0,0 +1,116 @@
/*************************************************************************/
/* plist.h */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
/* */
/* 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. */
/*************************************************************************/
// Property list file format (application/x-plist) parser, property list ASN-1 serialization.
#ifndef PLIST_H
#define PLIST_H
#include "core/crypto/crypto_core.h"
#include "core/io/file_access.h"
#include "modules/modules_enabled.gen.h" // For regex.
#ifdef MODULE_REGEX_ENABLED
class PListNode;
class PList : public RefCounted {
friend class PListNode;
public:
enum PLNodeType {
PL_NODE_TYPE_NIL,
PL_NODE_TYPE_STRING,
PL_NODE_TYPE_ARRAY,
PL_NODE_TYPE_DICT,
PL_NODE_TYPE_BOOLEAN,
PL_NODE_TYPE_INTEGER,
PL_NODE_TYPE_REAL,
PL_NODE_TYPE_DATA,
PL_NODE_TYPE_DATE,
};
private:
Ref<PListNode> root;
public:
PList();
PList(const String &p_string);
bool load_file(const String &p_filename);
bool load_string(const String &p_string);
PackedByteArray save_asn1() const;
String save_text() const;
Ref<PListNode> get_root();
};
/*************************************************************************/
class PListNode : public RefCounted {
static int _asn1_size_len(uint8_t p_len_octets);
public:
PList::PLNodeType data_type = PList::PLNodeType::PL_NODE_TYPE_NIL;
CharString data_string;
Vector<Ref<PListNode>> data_array;
Map<String, Ref<PListNode>> data_dict;
union {
int32_t data_int;
bool data_bool;
float data_real;
};
static Ref<PListNode> new_array();
static Ref<PListNode> new_dict();
static Ref<PListNode> new_string(const String &p_string);
static Ref<PListNode> new_data(const String &p_string);
static Ref<PListNode> new_date(const String &p_string);
static Ref<PListNode> new_bool(bool p_bool);
static Ref<PListNode> new_int(int32_t p_int);
static Ref<PListNode> new_real(float p_real);
bool push_subnode(const Ref<PListNode> &p_node, const String &p_key = "");
size_t get_asn1_size(uint8_t p_len_octets) const;
void store_asn1_size(PackedByteArray &p_stream, uint8_t p_len_octets) const;
bool store_asn1(PackedByteArray &p_stream, uint8_t p_len_octets) const;
void store_text(String &p_stream, uint8_t p_indent) const;
PListNode() {}
~PListNode() {}
};
#endif // MODULE_REGEX_ENABLED
#endif // PLIST_H