[macOS export] Implements ad-hoc signing on Linux/Windows, adds extra privacy settings, entitlements warnings and error checking.

This commit is contained in:
bruvzg 2021-07-30 13:41:24 +03:00
parent d6f972fad4
commit 463b92030f
16 changed files with 4028 additions and 109 deletions

View file

@ -114,6 +114,7 @@ if env["tools"]:
SConscript("fileserver/SCsub")
SConscript("icons/SCsub")
SConscript("import/SCsub")
SConscript("export/SCsub")
SConscript("plugins/SCsub")
lib = env.add_library("editor", env.editor_sources)

5
editor/export/SCsub Normal file
View file

@ -0,0 +1,5 @@
#!/usr/bin/env python
Import("env")
env.add_source_files(env.editor_sources, "*.cpp")

1610
editor/export/codesign.cpp Normal file

File diff suppressed because it is too large Load diff

356
editor/export/codesign.h Normal file
View file

@ -0,0 +1,356 @@
/*************************************************************************/
/* codesign.h */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2021 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.
#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/regex/regex.h"
#include "plist.h"
/*************************************************************************/
/* 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:
CodeSignSuperBlob();
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 /* CODESIGN_H */

237
editor/export/lipo.cpp Normal file
View file

@ -0,0 +1,237 @@
/*************************************************************************/
/* lipo.cpp */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2021 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 "lipo.h"
bool LipO::is_lipo(const String &p_path) {
FileAccessRef fb = FileAccess::open(p_path, FileAccess::READ);
ERR_FAIL_COND_V_MSG(!fb, false, "LipO: " + TTR("Can't open file: ") + 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, "LipO: " + TTR("Can't open file: ") + 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, "LipO: " + TTR("Invalid MachO file: ") + 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, "LipO: " + TTR("Can't open file: ") + 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, "LipO: " + TTR("Can't open file: ") + 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, "LipO: " + TTR("Can't open file: ") + 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, "LipO: " + TTR("Invalid fat binary: ") + p_path);
}
return true;
}
int LipO::get_arch_count() const {
ERR_FAIL_COND_V_MSG(!fa, 0, "LipO: " + TTR("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: " + TTR("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, "LipO: " + TTR("Can't open file: ") + 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();
}

71
editor/export/lipo.h Normal file
View file

@ -0,0 +1,71 @@
/*************************************************************************/
/* lipo.h */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2021 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 "macho.h"
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 /* LIPO_H */

550
editor/export/macho.cpp Normal file
View file

@ -0,0 +1,550 @@
/*************************************************************************/
/* macho.cpp */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2021 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 "macho.h"
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: " + TTR("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: " + TTR("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, "MachO: " + TTR("Can't open file: ") + 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, "MachO: " + TTR("Can't open file: ") + 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, "MachO: " + TTR("File is not a valid MachO binary: ") + 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, "MachO: " + TTR("No load commands or executable code found: ") + p_path);
}
return true;
}
uint64_t MachO::get_exe_base() {
ERR_FAIL_COND_V_MSG(!fa, 0, "MachO: " + TTR("File not opened."));
return exe_base;
}
uint64_t MachO::get_exe_limit() {
ERR_FAIL_COND_V_MSG(!fa, 0, "MachO: " + TTR("File not opened."));
return exe_limit;
}
int32_t MachO::get_align() {
ERR_FAIL_COND_V_MSG(!fa, 0, "MachO: " + TTR("File not opened."));
return align;
}
uint32_t MachO::get_cputype() {
ERR_FAIL_COND_V_MSG(!fa, 0, "MachO: " + TTR("File not opened."));
return cputype;
}
uint32_t MachO::get_cpusubtype() {
ERR_FAIL_COND_V_MSG(!fa, 0, "MachO: " + TTR("File not opened."));
return cpusubtype;
}
uint64_t MachO::get_size() {
ERR_FAIL_COND_V_MSG(!fa, 0, "MachO: " + TTR("File not opened."));
return fa->get_length();
}
uint64_t MachO::get_signature_offset() {
ERR_FAIL_COND_V_MSG(!fa, 0, "MachO: " + TTR("File not opened."));
ERR_FAIL_COND_V_MSG(signature_offset == 0, 0, "MachO: " + TTR("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: " + TTR("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: " + TTR("File not opened."));
ERR_FAIL_COND_V_MSG(signature_offset == 0, 0, "MachO: " + TTR("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: " + TTR("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: " + TTR("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: " + TTR("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: " + TTR("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: " + TTR("File not opened."));
// Ensure signature load command exists.
ERR_FAIL_COND_V_MSG(link_edit_offset == 0, false, "MachO: " + TTR("No __LINKEDIT segment found."));
ERR_FAIL_COND_V_MSG(!alloc_signature(p_size), false, "MachO: " + TTR("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: " + TTR("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;
}
}

212
editor/export/macho.h Normal file
View file

@ -0,0 +1,212 @@
/*************************************************************************/
/* macho.h */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2021 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"
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 /* MACHO_H */

564
editor/export/plist.cpp Normal file
View file

@ -0,0 +1,564 @@
/*************************************************************************/
/* plist.cpp */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2021 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 "plist.h"
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: " + TTR("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: " + TTR("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: " + TTR("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: {
//NOP
} 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: " + TTR("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: " + TTR("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: " + TTR("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: " + TTR("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: " + TTR("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: " + TTR("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: " + TTR("Mismatched <") + "/dict" + TTR("> 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: " + TTR("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: " + TTR("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: " + TTR("Mismatched <") + "/array" + TTR("> 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, "PList: " + TTR("Mismatched <") + token + TTR("> tag."));
}
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, "PList: " + TTR("Mismatched <") + token + TTR("> and <") + end_token + TTR("> tag pair."));
}
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: " + TTR("Invalid value type."));
}
if (stack.is_empty() || !stack.back()->get()->push_subnode(var, key)) {
ERR_FAIL_V_MSG(false, "PList: " + TTR("Can't push subnode, invalid parent type."));
}
}
}
if (!stack.is_empty() || !done_plist) {
ERR_FAIL_V_MSG(false, "PList: " + TTR("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: " + TTR("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: " + TTR("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: " + TTR("ASN.1 serializer error."));
}
return ret;
}
String PList::save_text() const {
if (root == nullptr) {
ERR_FAIL_V_MSG(String(), "PList: " + TTR("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;
}

111
editor/export/plist.h Normal file
View file

@ -0,0 +1,111 @@
/*************************************************************************/
/* plist.h */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2021 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"
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 /* PLIST_H */

View file

@ -252,12 +252,12 @@ void ProjectExportDialog::_edit_preset(int p_index) {
}
error += " - " + items[i];
}
export_error->set_text(error);
export_error->show();
} else {
export_error->hide();
}
export_warning->hide();
if (needs_templates) {
export_templates_error->show();
} else {
@ -266,8 +266,21 @@ void ProjectExportDialog::_edit_preset(int p_index) {
export_button->set_disabled(true);
get_ok_button()->set_disabled(true);
} else {
if (error != String()) {
Vector<String> items = error.split("\n", false);
error = "";
for (int i = 0; i < items.size(); i++) {
if (i > 0) {
error += "\n";
}
error += " - " + items[i];
}
export_warning->set_text(error);
export_warning->show();
} else {
export_warning->hide();
}
export_error->hide();
export_templates_error->hide();
export_button->set_disabled(false);
@ -1246,6 +1259,11 @@ ProjectExportDialog::ProjectExportDialog() {
export_error->hide();
export_error->add_theme_color_override("font_color", EditorNode::get_singleton()->get_gui_base()->get_theme_color(SNAME("error_color"), SNAME("Editor")));
export_warning = memnew(Label);
main_vb->add_child(export_warning);
export_warning->hide();
export_warning->add_theme_color_override("font_color", EditorNode::get_singleton()->get_gui_base()->get_theme_color(SNAME("warning_color"), SNAME("Editor")));
export_templates_error = memnew(HBoxContainer);
main_vb->add_child(export_templates_error);
export_templates_error->hide();

View file

@ -99,6 +99,7 @@ private:
Label *script_key_error;
Label *export_error;
Label *export_warning;
HBoxContainer *export_templates_error;
String default_filename;

View file

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

View file

@ -33,6 +33,9 @@
#include "export_plugin.h"
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;
platform.instantiate();

View file

@ -58,15 +58,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/copyright"), ""));
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/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));
#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::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/hardened_runtime"), true));
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "codesign/entitlements/custom_file", PROPERTY_HINT_GLOBAL_FILE, "*.plist"), ""));
if (!Engine::get_singleton()->has_singleton("GodotSharp")) {
@ -97,6 +107,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::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::BOOL, "notarization/enable"), false));
@ -305,13 +316,56 @@ void EditorExportPlatformOSX::_fix_plist(const Ref<EditorExportPreset> &p_preset
} else if (lines[i].find("$copyright") != -1) {
strnew += lines[i].replace("$copyright", p_preset->get("application/copyright")) + "\n";
} else if (lines[i].find("$highres") != -1) {
strnew += lines[i].replace("$highres", p_preset->get("display/high_res") ? "<true/>" : "<false/>") + "\n";
} else if (lines[i].find("$camera_usage_description") != -1) {
String description = p_preset->get("privacy/camera_usage_description");
strnew += lines[i].replace("$camera_usage_description", description) + "\n";
} else if (lines[i].find("$microphone_usage_description") != -1) {
String description = p_preset->get("privacy/microphone_usage_description");
strnew += lines[i].replace("$microphone_usage_description", description) + "\n";
strnew += lines[i].replace("$highres", p_preset->get("display/high_res") ? "\t<true/>" : "\t<false/>") + "\n";
} else if (lines[i].find("$usage_descriptions") != -1) {
String descriptions;
if (!((String)p_preset->get("privacy/microphone_usage_description")).is_empty()) {
descriptions += "\t<key>NSMicrophoneUsageDescription</key>\n";
descriptions += "\t<string>" + (String)p_preset->get("privacy/microphone_usage_description") + "</string>\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 {
strnew += lines[i] + "\n";
}
@ -362,14 +416,16 @@ Error EditorExportPlatformOSX::_notarize(const Ref<EditorExportPreset> &p_preset
Error err = OS::get_singleton()->execute("xcrun", args, &str, nullptr, true);
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) {
EditorNode::add_io_error("altool: " + str);
return FAILED;
} 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(" 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(TTR("Note: The notarization process generally takes less than an hour. When the process is completed, you'll receive an email."));
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(" " + TTR("Run the following command to staple notarization ticket to the exported application (optional):"));
print_line(" \"xcrun stapler staple <app path>\"");
}
#endif
@ -378,61 +434,70 @@ 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) {
#ifdef OSX_ENABLED
List<String> args;
if (p_preset->get("codesign/timestamp")) {
args.push_back("--timestamp");
}
if (p_preset->get("codesign/hardened_runtime")) {
args.push_back("--options");
args.push_back("runtime");
}
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);
bool force_builtin_codesign = EditorSettings::get_singleton()->get("export/macos/force_builtin_codesign");
if ((!FileAccess::exists("/usr/bin/codesign") && !FileAccess::exists("/bin/codesign")) || force_builtin_codesign) {
print_verbose("using built-in codesign...");
String error_msg;
Error err = CodeSign::codesign(p_preset->get("codesign/hardened_runtime"), p_preset->get("codesign/replace_existing_signature"), p_path, p_ent_path, error_msg);
if (err != OK) {
EditorNode::add_io_error("Built-in CodeSign: " + error_msg);
return FAILED;
}
}
args.push_back("-s");
if (p_preset->get("codesign/identity") == "") {
args.push_back("-");
return OK;
} else {
args.push_back(p_preset->get("codesign/identity"));
print_verbose("using external codesign...");
List<String> args;
if (p_preset->get("codesign/timestamp")) {
args.push_back("--timestamp");
}
if (p_preset->get("codesign/hardened_runtime")) {
args.push_back("--options");
args.push_back("runtime");
}
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 (p_preset->get("codesign/identity") == "") {
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;
}
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
return OK;
}
Error EditorExportPlatformOSX::_create_dmg(const String &p_dmg_path, const String &p_pkg_name, const String &p_app_path_name) {
@ -455,12 +520,12 @@ Error EditorExportPlatformOSX::_create_dmg(const String &p_dmg_path, const Strin
Error err = OS::get_singleton()->execute("hdiutil", args, &str, nullptr, true);
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("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 {
EditorNode::add_io_error("hdiutil: create failed");
EditorNode::add_io_error("hdiutil: " + TTR("DMG create failed."));
}
return FAILED;
}
@ -497,13 +562,13 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p
FileAccess *src_f = nullptr;
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;
}
unzFile src_pkg_zip = unzOpen2(src_pkg_name.utf8().get_data(), &io);
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;
}
@ -522,12 +587,27 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p
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.
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);
print_line("Exporting to " + tmp_app_path_name);
String 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;
@ -536,16 +616,22 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p
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");
// Create our folder structure.
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");
}
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");
}
@ -555,7 +641,7 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p
}
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");
}
@ -584,6 +670,25 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p
// Write.
file = file.replace_first("osx_template.app/", "");
if (((info.external_fa >> 16L) & 0120000) == 0120000) {
#ifndef UNIX_ENABLED
WARN_PRINT(vformat(TTR("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") {
_fix_plist(p_preset, data, pkg_name);
}
@ -647,7 +752,7 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p
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.
file = tmp_app_path_name.plus_file(file);
@ -677,12 +782,12 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p
unzClose(src_pkg_zip);
if (!found_binary) {
ERR_PRINT("Requested template binary '" + binary_to_use + "' not found. It might be missing from your template archive.");
ERR_PRINT(vformat(TTR("Requested template binary '%s' not found. It might be missing from your template archive."), binary_to_use));
err = ERR_FILE_NOT_FOUND;
}
if (err == OK) {
if (ep.step("Making PKG", 1)) {
if (ep.step(TTR("Making PKG"), 1)) {
return ERR_SKIP;
}
@ -861,13 +966,28 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p
}
}
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(TTR("Application with an ad-hoc signature require 'Disable Library Validation' entitlement to load dynamic libraries."));
err = ERR_CANT_CREATE;
}
}
if (err == OK) {
DirAccessRef da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
for (int i = 0; i < shared_objects.size(); i++) {
String src_path = ProjectSettings::get_singleton()->globalize_path(shared_objects[i].path);
if (da->dir_exists(src_path)) {
#ifndef UNIX_ENABLED
WARN_PRINT("Relative symlinks are not supported, exported " + src_path.get_file() + " might be broken!");
WARN_PRINT(vformat(TTR("Relative symlinks are not supported on this OS, exported framework '%s' might be broken!"), src_path.get_file()));
#endif
print_verbose("export framework: " + src_path + " -> " + tmp_app_path_name + "/Contents/Frameworks/" + src_path.get_file());
err = da->make_dir_recursive(tmp_app_path_name + "/Contents/Frameworks/" + src_path.get_file());
@ -893,31 +1013,31 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p
}
if (err == OK && sign_enabled) {
if (ep.step("Code signing bundle", 2)) {
if (ep.step(TTR("Code signing bundle"), 2)) {
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") {
// Create a DMG.
if (err == OK) {
if (ep.step("Making DMG", 3)) {
if (ep.step(TTR("Making DMG"), 3)) {
return ERR_SKIP;
}
err = _create_dmg(p_path, pkg_name, tmp_app_path_name);
}
// Sign DMG.
if (err == OK && sign_enabled) {
if (ep.step("Code signing DMG", 3)) {
if (err == OK && sign_enabled && !ad_hoc) {
if (ep.step(TTR("Code signing DMG"), 3)) {
return ERR_SKIP;
}
err = _code_sign(p_preset, p_path, ent_path);
}
} else {
} else if (export_format == "zip") {
// Create ZIP.
if (err == OK) {
if (ep.step("Making ZIP", 3)) {
if (ep.step(TTR("Making ZIP"), 3)) {
return ERR_SKIP;
}
if (FileAccess::exists(p_path)) {
@ -936,20 +1056,30 @@ Error EditorExportPlatformOSX::export_project(const Ref<EditorExportPreset> &p_p
bool noto_enabled = p_preset->get("notarization/enable");
if (err == OK && noto_enabled) {
if (ep.step("Sending archive for notarization", 4)) {
return ERR_SKIP;
if (export_format == "app") {
WARN_PRINT(TTR("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.
DirAccess::remove_file_or_error(hlp_ent_path);
// Clean up temporary .app dir.
tmp_app_dir->change_dir(tmp_app_path_name);
tmp_app_dir->erase_contents_recursive();
tmp_app_dir->change_dir("..");
tmp_app_dir->remove(tmp_app_dir_name);
// Clean up temporary .app dir and generated entitlements.
if ((String)(p_preset->get("codesign/entitlements/custom_file")) == "") {
tmp_app_dir->remove(ent_path);
}
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;
@ -1090,14 +1220,32 @@ bool EditorExportPlatformOSX::can_export(const Ref<EditorExportPreset> &p_preset
bool sign_enabled = p_preset->get("codesign/enable");
bool noto_enabled = p_preset->get("notarization/enable");
if (!sign_enabled) {
err += TTR("Warning: Code signing is disabled. Exported project will not run on Macs with enabled Gatekeeper and Apple Silicon powered Macs.") + "\n";
}
#ifdef OSX_ENABLED
bool ad_hoc = ((p_preset->get("codesign/identity") == "") || (p_preset->get("codesign/identity") == "-"));
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 (!sign_enabled) {
err += TTR("Notarization: code signing required.") + "\n";
err += TTR("Notarization: Code signing is required for notarization.") + "\n";
valid = false;
}
bool hr_enabled = p_preset->get("codesign/hardened_runtime");
if (!hr_enabled) {
err += TTR("Notarization: hardened runtime required.") + "\n";
if (!(bool)p_preset->get("codesign/hardened_runtime")) {
err += TTR("Notarization: Hardened runtime is required for notarization.") + "\n";
valid = false;
}
if (!(bool)p_preset->get("codesign/timestamp")) {
err += TTR("Notarization: Timestamp runtime is required for notarization.") + "\n";
valid = false;
}
if (p_preset->get("notarization/apple_id_name") == "") {
@ -1108,6 +1256,39 @@ bool EditorExportPlatformOSX::can_export(const Ref<EditorExportPreset> &p_preset
err += TTR("Notarization: Apple ID password not specified.") + "\n";
valid = false;
}
} else {
#ifdef OSX_ENABLED
err += TTR("Warning: Notarization is disabled. The 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. The exported project will be blocked by Gatekeeper, if it's downloaded from an unknown source.") + "\n";
#endif
}
if (sign_enabled) {
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()) {

View file

@ -42,6 +42,7 @@
#include "editor/editor_export.h"
#include "editor/editor_node.h"
#include "editor/editor_settings.h"
#include "editor/export/codesign.h"
#include "platform/osx/logo.gen.h"
#include <sys/stat.h>
@ -61,13 +62,13 @@ class EditorExportPlatformOSX : public EditorExportPlatform {
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);
#ifdef OSX_ENABLED
bool use_codesign() const { return true; }
#ifdef OSX_ENABLED
bool use_dmg() const { return true; }
#else
bool use_codesign() const { return false; }
bool use_dmg() const { return false; }
#endif
bool is_package_name_valid(const String &p_package, String *r_error = nullptr) const {
String pname = p_package;
@ -106,6 +107,7 @@ public:
list.push_back("dmg");
}
list.push_back("zip");
list.push_back("app");
return list;
}
virtual Error export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags = 0) override;