Preserve data across the Straywild transition
This commit is contained in:
parent
737d1cf2e0
commit
98a27e2f53
28 changed files with 577 additions and 94 deletions
|
|
@ -21,6 +21,7 @@ signal friend_invite_finished(success: bool, message: String)
|
|||
|
||||
const BASE_URL_SETTING: String = "network/discovery/base_url"
|
||||
const BASE_URL_ENVIRONMENT: String = "straywild_DISCOVERY_URL"
|
||||
const LEGACY_BASE_URL_ENVIRONMENT: String = "NETFISHING_DISCOVERY_URL"
|
||||
const SETTINGS_PATH: String = "user://network_discovery.cfg"
|
||||
const DEFAULT_ROOM_NAME: String = "Player's Server"
|
||||
const LEGACY_DEFAULT_ROOM_NAME: String = "straywild Room"
|
||||
|
|
@ -553,6 +554,10 @@ func is_own_room(room: Dictionary) -> bool:
|
|||
|
||||
func _configured_base_url() -> String:
|
||||
var value: String = OS.get_environment(BASE_URL_ENVIRONMENT).strip_edges()
|
||||
if value.is_empty():
|
||||
value = OS.get_environment(
|
||||
LEGACY_BASE_URL_ENVIRONMENT
|
||||
).strip_edges()
|
||||
if value.is_empty():
|
||||
value = str(ProjectSettings.get_setting(BASE_URL_SETTING, "")).strip_edges()
|
||||
while value.ends_with("/"):
|
||||
|
|
|
|||
|
|
@ -17,6 +17,10 @@ var _data_root: PlayerDataRoot
|
|||
func configure_storage(path: String, data_root: PlayerDataRoot) -> void:
|
||||
_store_path = path
|
||||
_data_root = data_root
|
||||
_namespaces.clear()
|
||||
_loaded = false
|
||||
_write_blocked = false
|
||||
_expected_hash = ""
|
||||
|
||||
|
||||
func is_banned(host_fingerprint: String, target_fingerprint: String) -> bool:
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ extends Node
|
|||
signal operation_finished(success: bool, message: String)
|
||||
|
||||
const MAGIC := "straywild_IDENTITY_BACKUP"
|
||||
const LEGACY_MAGIC := "NETFISHING_IDENTITY_BACKUP"
|
||||
const FORMAT_VERSION := 1
|
||||
const MAX_BACKUP_BYTES := 256 * 1024
|
||||
const MIN_PASSPHRASE_LENGTH := 12
|
||||
|
|
@ -110,8 +111,9 @@ func inspect_backup(
|
|||
if json.parse(text) != OK or typeof(json.data) != TYPE_DICTIONARY:
|
||||
return {"ok": false}
|
||||
var data: Dictionary = json.data
|
||||
var legacy_backup: bool = data.get("magic") == LEGACY_MAGIC
|
||||
if (
|
||||
data.get("magic") != MAGIC
|
||||
data.get("magic") not in [MAGIC, LEGACY_MAGIC]
|
||||
or data.get("format_version") != FORMAT_VERSION
|
||||
or data.get("identity_type") != expected_type
|
||||
or data.get("algorithm") != NetworkIdentityCrypto.ALGORITHM
|
||||
|
|
@ -134,12 +136,23 @@ func inspect_backup(
|
|||
var signature: PackedByteArray = Marshalls.base64_to_raw(
|
||||
str(data.get("self_signature", ""))
|
||||
)
|
||||
if not NetworkIdentityCrypto.verify_fields(
|
||||
NetworkIdentityCrypto.load_public_key(public_pem),
|
||||
"identity_backup_self_test",
|
||||
[expected_type, fingerprint],
|
||||
signature,
|
||||
):
|
||||
var public_key: CryptoKey = NetworkIdentityCrypto.load_public_key(public_pem)
|
||||
var backup_signature_valid: bool = (
|
||||
NetworkIdentityCrypto.verify_legacy_fields(
|
||||
public_key,
|
||||
"identity_backup_self_test",
|
||||
[expected_type, fingerprint],
|
||||
signature,
|
||||
)
|
||||
if legacy_backup
|
||||
else NetworkIdentityCrypto.verify_fields(
|
||||
public_key,
|
||||
"identity_backup_self_test",
|
||||
[expected_type, fingerprint],
|
||||
signature,
|
||||
)
|
||||
)
|
||||
if not backup_signature_valid:
|
||||
return {"ok": false}
|
||||
var fresh: PackedByteArray = NetworkIdentityCrypto.sign_fields(
|
||||
key, "identity_import_self_test", [fingerprint]
|
||||
|
|
|
|||
|
|
@ -15,6 +15,10 @@ var _data_root: PlayerDataRoot
|
|||
func configure_storage(path: String, data_root: PlayerDataRoot) -> void:
|
||||
_store_path = path
|
||||
_data_root = data_root
|
||||
_records.clear()
|
||||
_loaded = false
|
||||
_write_blocked = false
|
||||
_expected_hash = ""
|
||||
|
||||
|
||||
func observe(fingerprint: String, display_name: String) -> String:
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ const FINGERPRINT_LENGTH: int = 64
|
|||
const MAX_PUBLIC_KEY_BYTES: int = 8192
|
||||
const MAX_SIGNATURE_BYTES: int = 1024
|
||||
const DOMAIN_PREFIX: String = "straywild"
|
||||
const LEGACY_DOMAIN_PREFIX: String = "NETFISHING"
|
||||
const IDENTITY_VERSION: String = "identity_v1"
|
||||
|
||||
|
||||
|
|
@ -50,8 +51,16 @@ static func secure_id(byte_count: int = 16) -> String:
|
|||
|
||||
|
||||
static func canonical_bytes(domain: String, fields: Array) -> PackedByteArray:
|
||||
return canonical_bytes_for_prefix(DOMAIN_PREFIX, domain, fields)
|
||||
|
||||
|
||||
static func canonical_bytes_for_prefix(
|
||||
prefix: String,
|
||||
domain: String,
|
||||
fields: Array,
|
||||
) -> PackedByteArray:
|
||||
var output := PackedByteArray()
|
||||
_append_string(output, DOMAIN_PREFIX)
|
||||
_append_string(output, prefix)
|
||||
_append_string(output, IDENTITY_VERSION)
|
||||
_append_string(output, domain)
|
||||
for value: Variant in fields:
|
||||
|
|
@ -117,6 +126,37 @@ static func verify_fields(
|
|||
)
|
||||
|
||||
|
||||
static func verify_legacy_fields(
|
||||
public_key: CryptoKey,
|
||||
domain: String,
|
||||
fields: Array,
|
||||
signature: PackedByteArray,
|
||||
) -> bool:
|
||||
if (
|
||||
public_key == null
|
||||
or signature.is_empty()
|
||||
or signature.size() > MAX_SIGNATURE_BYTES
|
||||
):
|
||||
return false
|
||||
var bytes: PackedByteArray = canonical_bytes_for_prefix(
|
||||
LEGACY_DOMAIN_PREFIX,
|
||||
domain,
|
||||
fields,
|
||||
)
|
||||
if bytes.is_empty():
|
||||
return false
|
||||
var context := HashingContext.new()
|
||||
if context.start(HashingContext.HASH_SHA256) != OK:
|
||||
return false
|
||||
context.update(bytes)
|
||||
return Crypto.new().verify(
|
||||
HashingContext.HASH_SHA256,
|
||||
context.finish(),
|
||||
signature,
|
||||
public_key,
|
||||
)
|
||||
|
||||
|
||||
static func load_public_key(public_pem: String) -> CryptoKey:
|
||||
var normalized := normalize_public_pem(public_pem)
|
||||
if normalized.is_empty() or normalized.to_utf8_buffer().size() > MAX_PUBLIC_KEY_BYTES:
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ var _future_version := false
|
|||
func configure_storage(path: String, data_root: PlayerDataRoot) -> void:
|
||||
_profile_path = path
|
||||
_data_root = data_root
|
||||
_expected_hash = ""
|
||||
_future_version = false
|
||||
|
||||
|
||||
func _temp_path() -> String:
|
||||
|
|
|
|||
|
|
@ -51,6 +51,17 @@ func setup(
|
|||
_apply_local_voice_to_avatar()
|
||||
|
||||
|
||||
func reload_persisted_profile() -> bool:
|
||||
if (
|
||||
not _preferences.load_or_create()
|
||||
or not _appearance_store.load_preferences()
|
||||
):
|
||||
return false
|
||||
_apply_to_avatar(1, _appearance_store.get_snapshot())
|
||||
_apply_local_voice_to_avatar()
|
||||
return true
|
||||
|
||||
|
||||
func get_persisted_name() -> String:
|
||||
return _preferences.display_name
|
||||
|
||||
|
|
|
|||
|
|
@ -10,11 +10,41 @@ const BOOTSTRAP_VERSION := 1
|
|||
const MANIFEST_VERSION := 1
|
||||
const LAYOUT_VERSION := 1
|
||||
const MANIFEST_FILENAME := "straywild_data.json"
|
||||
const LEGACY_MANIFEST_FILENAME := "netfishing_data.json"
|
||||
const README_FILENAME := "README.txt"
|
||||
const ENVIRONMENT_VARIABLE := "straywild_DATA_DIR"
|
||||
const CREATE_ENVIRONMENT_VARIABLE := "straywild_CREATE_DATA_DIR"
|
||||
const LEGACY_ENVIRONMENT_VARIABLE := "NETFISHING_DATA_DIR"
|
||||
const LEGACY_CREATE_ENVIRONMENT_VARIABLE := "NETFISHING_CREATE_DATA_DIR"
|
||||
const APPLICATION_ID := "straywild"
|
||||
const LEGACY_APPLICATION_ID := "netfishing"
|
||||
const APP_DATA_PORTABLE_PATH := "user://portable-data"
|
||||
const LEGACY_USER_DIRECTORY_NAME := "NETFISHING"
|
||||
const LEGACY_USER_FILES := [
|
||||
"data_root_bootstrap.json",
|
||||
"data_root_bootstrap.json.backup",
|
||||
"network_discovery.cfg",
|
||||
"player_settings.json",
|
||||
"player_settings.json.backup",
|
||||
"controller_mappings.json",
|
||||
"controller_mappings.json.backup",
|
||||
"keyboard_mouse_bindings.json",
|
||||
"keyboard_mouse_bindings.json.backup",
|
||||
"player_save.json",
|
||||
"network_profile.json",
|
||||
"player_appearance.json",
|
||||
"saved_servers.json",
|
||||
"known_players.json",
|
||||
"player_relationships.json",
|
||||
"server_trust.json",
|
||||
"host_bans.json",
|
||||
"player_identity.key",
|
||||
"player_identity.pub",
|
||||
"player_identity.json",
|
||||
"host_identity.key",
|
||||
"host_identity.pub",
|
||||
"host_identity.json",
|
||||
]
|
||||
const PATH_ALLOWED: StringName = &"allowed"
|
||||
const PATH_SOURCE_PROJECT: StringName = &"source_project"
|
||||
const PATH_INSTALLATION: StringName = &"installation"
|
||||
|
|
@ -37,23 +67,29 @@ var override_active := false
|
|||
|
||||
|
||||
func resolve() -> bool:
|
||||
prepare_legacy_user_data()
|
||||
_load_bootstrap_identity()
|
||||
var command_line: String = _command_line_override()
|
||||
if not command_line.is_empty():
|
||||
override_active = true
|
||||
mode = Mode.COMMAND_LINE_OVERRIDE
|
||||
return _activate_existing(command_line, "", false)
|
||||
if OS.has_environment(ENVIRONMENT_VARIABLE):
|
||||
var environment_variable: String = _available_environment_variable(
|
||||
ENVIRONMENT_VARIABLE,
|
||||
LEGACY_ENVIRONMENT_VARIABLE,
|
||||
)
|
||||
if not environment_variable.is_empty():
|
||||
override_active = true
|
||||
mode = Mode.ENVIRONMENT_OVERRIDE
|
||||
var environment_path: String = OS.get_environment(ENVIRONMENT_VARIABLE)
|
||||
var environment_path: String = OS.get_environment(environment_variable)
|
||||
if not environment_path.is_absolute_path():
|
||||
return _fail("straywild_DATA_DIR must be an absolute path.")
|
||||
if (
|
||||
OS.get_environment(CREATE_ENVIRONMENT_VARIABLE) == "1"
|
||||
and not FileAccess.file_exists(
|
||||
environment_path.path_join(MANIFEST_FILENAME)
|
||||
_environment_flag(
|
||||
CREATE_ENVIRONMENT_VARIABLE,
|
||||
LEGACY_CREATE_ENVIRONMENT_VARIABLE,
|
||||
)
|
||||
and not has_data_manifest(environment_path)
|
||||
):
|
||||
var created: Dictionary = create_unbound_root(environment_path)
|
||||
if not bool(created.get("ok", false)):
|
||||
|
|
@ -80,6 +116,30 @@ func resolve() -> bool:
|
|||
return false
|
||||
|
||||
|
||||
func prepare_legacy_user_data() -> void:
|
||||
var current_root: String = _normalize(
|
||||
ProjectSettings.globalize_path("user://")
|
||||
)
|
||||
if current_root.is_empty():
|
||||
return
|
||||
var legacy_root: String = _normalize(
|
||||
current_root.get_base_dir().path_join(LEGACY_USER_DIRECTORY_NAME)
|
||||
)
|
||||
if legacy_root == current_root or not DirAccess.dir_exists_absolute(legacy_root):
|
||||
return
|
||||
for filename: String in LEGACY_USER_FILES:
|
||||
var destination: String = current_root.path_join(filename)
|
||||
if FileAccess.file_exists(destination):
|
||||
continue
|
||||
var source: String = legacy_root.path_join(filename)
|
||||
if FileAccess.file_exists(source):
|
||||
_copy_legacy_user_file(source, destination)
|
||||
|
||||
|
||||
func has_data_manifest(path: String) -> bool:
|
||||
return not _existing_manifest_path(_normalize(path)).is_empty()
|
||||
|
||||
|
||||
func default_visible_path() -> String:
|
||||
var documents: String = OS.get_system_dir(OS.SYSTEM_DIR_DOCUMENTS)
|
||||
if documents.is_empty():
|
||||
|
|
@ -105,12 +165,14 @@ func select_new_root(path: String, app_data: bool = false) -> bool:
|
|||
normalized = ProjectSettings.globalize_path(APP_DATA_PORTABLE_PATH)
|
||||
if not _validate_candidate(normalized, true):
|
||||
return false
|
||||
var manifest_path: String = normalized.path_join(MANIFEST_FILENAME)
|
||||
if FileAccess.file_exists(manifest_path):
|
||||
var manifest_path: String = _existing_manifest_path(normalized)
|
||||
if not manifest_path.is_empty():
|
||||
var manifest: Dictionary = _read_json(manifest_path)
|
||||
if not _valid_manifest(manifest):
|
||||
return _fail("The selected folder has a malformed straywild manifest.")
|
||||
root_id = str(manifest["root_id"])
|
||||
if not _upgrade_legacy_manifest(normalized, manifest, manifest_path):
|
||||
return _fail("The legacy data-folder manifest could not be upgraded.")
|
||||
else:
|
||||
if not _directory_is_empty(normalized) and not app_data:
|
||||
return _fail(
|
||||
|
|
@ -145,14 +207,14 @@ func create_app_data_layout_for_migration(path: String) -> Dictionary:
|
|||
var normalized: String = _normalize(path)
|
||||
if not _validate_candidate(normalized, false):
|
||||
return {"ok": false, "message": error_message}
|
||||
var manifest_path: String = normalized.path_join(MANIFEST_FILENAME)
|
||||
if FileAccess.file_exists(manifest_path):
|
||||
var manifest_path: String = _existing_manifest_path(normalized)
|
||||
if not manifest_path.is_empty():
|
||||
var manifest: Dictionary = _read_json(manifest_path)
|
||||
return (
|
||||
{"ok": true, "root_id": str(manifest.get("root_id", ""))}
|
||||
if _valid_manifest(manifest)
|
||||
else {"ok": false, "message": "The app-data manifest is malformed."}
|
||||
)
|
||||
if not _valid_manifest(manifest):
|
||||
return {"ok": false, "message": "The app-data manifest is malformed."}
|
||||
if not _upgrade_legacy_manifest(normalized, manifest, manifest_path):
|
||||
return {"ok": false, "message": "The app-data manifest could not be upgraded."}
|
||||
return {"ok": true, "root_id": str(manifest.get("root_id", ""))}
|
||||
var id: String = Crypto.new().generate_random_bytes(16).hex_encode()
|
||||
if not _create_layout(normalized, id):
|
||||
return {"ok": false, "message": "The app-data layout could not be created."}
|
||||
|
|
@ -236,12 +298,14 @@ func _activate_existing(path: String, expected_id: String, permit_creation: bool
|
|||
var normalized: String = _normalize(path)
|
||||
if not _validate_candidate(normalized, permit_creation):
|
||||
return false
|
||||
var manifest_path: String = normalized.path_join(MANIFEST_FILENAME)
|
||||
if not FileAccess.file_exists(manifest_path):
|
||||
var manifest_path: String = _existing_manifest_path(normalized)
|
||||
if manifest_path.is_empty():
|
||||
return _fail("The selected folder is not a straywild data folder.")
|
||||
var manifest: Dictionary = _read_json(manifest_path)
|
||||
if not _valid_manifest(manifest):
|
||||
return _fail("The straywild data-folder manifest is malformed.")
|
||||
if not _upgrade_legacy_manifest(normalized, manifest, manifest_path):
|
||||
return _fail("The legacy data-folder manifest could not be upgraded.")
|
||||
var found_id: String = str(manifest["root_id"])
|
||||
if not expected_id.is_empty() and expected_id != found_id:
|
||||
return _fail("The selected data folder does not match this device pointer.")
|
||||
|
|
@ -422,12 +486,71 @@ func _valid_manifest(data: Dictionary) -> bool:
|
|||
return (
|
||||
data.get("format_version") == MANIFEST_VERSION
|
||||
and data.get("layout_version") == LAYOUT_VERSION
|
||||
and data.get("application") == APPLICATION_ID
|
||||
and data.get("application") in [APPLICATION_ID, LEGACY_APPLICATION_ID]
|
||||
and typeof(data.get("root_id")) == TYPE_STRING
|
||||
and str(data["root_id"]).length() == 32
|
||||
)
|
||||
|
||||
|
||||
func _existing_manifest_path(path: String) -> String:
|
||||
var current: String = path.path_join(MANIFEST_FILENAME)
|
||||
if FileAccess.file_exists(current):
|
||||
return current
|
||||
var legacy: String = path.path_join(LEGACY_MANIFEST_FILENAME)
|
||||
return legacy if FileAccess.file_exists(legacy) else ""
|
||||
|
||||
|
||||
func _upgrade_legacy_manifest(
|
||||
path: String,
|
||||
manifest: Dictionary,
|
||||
manifest_path: String,
|
||||
) -> bool:
|
||||
if (
|
||||
manifest_path.get_file() == MANIFEST_FILENAME
|
||||
and manifest.get("application") == APPLICATION_ID
|
||||
):
|
||||
return true
|
||||
var upgraded: Dictionary = manifest.duplicate(true)
|
||||
upgraded["application"] = APPLICATION_ID
|
||||
upgraded["last_opened_at_unix"] = int(Time.get_unix_time_from_system())
|
||||
return _write_text_atomic(
|
||||
path.path_join(MANIFEST_FILENAME),
|
||||
JSON.stringify(upgraded, "\t"),
|
||||
)
|
||||
|
||||
|
||||
func _available_environment_variable(primary: String, legacy: String) -> String:
|
||||
if OS.has_environment(primary):
|
||||
return primary
|
||||
return legacy if OS.has_environment(legacy) else ""
|
||||
|
||||
|
||||
func _environment_flag(primary: String, legacy: String) -> bool:
|
||||
var variable: String = _available_environment_variable(primary, legacy)
|
||||
return not variable.is_empty() and OS.get_environment(variable) == "1"
|
||||
|
||||
|
||||
func _copy_legacy_user_file(source: String, destination: String) -> bool:
|
||||
var bytes: PackedByteArray = PortableFileGuard.read_bytes(
|
||||
source,
|
||||
16 * 1024 * 1024,
|
||||
)
|
||||
if bytes.is_empty() and FileAccess.get_open_error() != OK:
|
||||
return false
|
||||
if DirAccess.make_dir_recursive_absolute(destination.get_base_dir()) != OK:
|
||||
return false
|
||||
var file: FileAccess = FileAccess.open(destination, FileAccess.WRITE)
|
||||
if file == null:
|
||||
return false
|
||||
file.store_buffer(bytes)
|
||||
file.flush()
|
||||
var copied: bool = file.get_error() == OK
|
||||
file.close()
|
||||
if copied and source.ends_with(".key"):
|
||||
FileAccess.set_unix_permissions(destination, 384)
|
||||
return copied
|
||||
|
||||
|
||||
func _directory_is_empty(path: String) -> bool:
|
||||
var access: DirAccess = DirAccess.open(path)
|
||||
if access == null:
|
||||
|
|
|
|||
|
|
@ -21,6 +21,10 @@ var _data_root: PlayerDataRoot
|
|||
func configure_storage(path: String, data_root: PlayerDataRoot) -> void:
|
||||
_store_path = path
|
||||
_data_root = data_root
|
||||
_records.clear()
|
||||
_loaded = false
|
||||
_write_blocked = false
|
||||
_expected_hash = ""
|
||||
|
||||
|
||||
func is_muted(fingerprint: String) -> bool:
|
||||
|
|
|
|||
|
|
@ -26,10 +26,7 @@ static func migrate_legacy_to(
|
|||
) -> Dictionary:
|
||||
var normalized: String = destination.simplify_path().trim_suffix("/")
|
||||
if DirAccess.dir_exists_absolute(normalized):
|
||||
var existing_manifest: String = normalized.path_join(
|
||||
PlayerDataRoot.MANIFEST_FILENAME
|
||||
)
|
||||
if FileAccess.file_exists(existing_manifest):
|
||||
if data_root.has_data_manifest(normalized):
|
||||
return {
|
||||
"ok": false,
|
||||
"requires_existing_root_decision": true,
|
||||
|
|
@ -106,7 +103,7 @@ static func migrate_active_to(
|
|||
"message": "This data folder is already active.",
|
||||
}
|
||||
if DirAccess.dir_exists_absolute(normalized) and not _directory_empty(normalized):
|
||||
if FileAccess.file_exists(normalized.path_join(PlayerDataRoot.MANIFEST_FILENAME)):
|
||||
if data_root.has_data_manifest(normalized):
|
||||
return {
|
||||
"ok": false,
|
||||
"requires_existing_root_decision": true,
|
||||
|
|
@ -207,8 +204,7 @@ static func replace_existing_with_legacy(
|
|||
destination: String,
|
||||
) -> Dictionary:
|
||||
var normalized: String = destination.simplify_path().trim_suffix("/")
|
||||
var manifest: String = normalized.path_join(PlayerDataRoot.MANIFEST_FILENAME)
|
||||
if not FileAccess.file_exists(manifest):
|
||||
if not data_root.has_data_manifest(normalized):
|
||||
return {"ok": false, "message": "The selected folder is not a straywild data root."}
|
||||
var recovery: String = ProjectSettings.globalize_path(
|
||||
"user://migration-recovery"
|
||||
|
|
@ -229,7 +225,7 @@ static func replace_existing_with_active(
|
|||
destination: String,
|
||||
) -> Dictionary:
|
||||
var normalized: String = destination.simplify_path().trim_suffix("/")
|
||||
if not FileAccess.file_exists(normalized.path_join(PlayerDataRoot.MANIFEST_FILENAME)):
|
||||
if not data_root.has_data_manifest(normalized):
|
||||
return {"ok": false, "message": "The selected folder is not a straywild data root."}
|
||||
var recovery: String = ProjectSettings.globalize_path(
|
||||
"user://migration-recovery"
|
||||
|
|
|
|||
|
|
@ -22,6 +22,12 @@ var _data_root: PlayerDataRoot
|
|||
func configure_storage(path: String, data_root: PlayerDataRoot) -> void:
|
||||
_store_path = path
|
||||
_data_root = data_root
|
||||
_saved_entries.clear()
|
||||
_recent_entries.clear()
|
||||
_loaded = false
|
||||
_recovery_warning = ""
|
||||
_write_blocked = false
|
||||
_expected_hash = ""
|
||||
|
||||
|
||||
func _temp_path() -> String:
|
||||
|
|
|
|||
|
|
@ -19,6 +19,10 @@ var _data_root: PlayerDataRoot
|
|||
func configure_storage(path: String, data_root: PlayerDataRoot) -> void:
|
||||
_store_path = path
|
||||
_data_root = data_root
|
||||
_records.clear()
|
||||
_loaded = false
|
||||
_write_blocked = false
|
||||
_expected_hash = ""
|
||||
|
||||
|
||||
func verify(endpoint: ConnectionEndpoint, fingerprint: String) -> Verification:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue