Use the public rename API for an explicit empty existing name, retain omitted names and raw listing values, and verify fresh unnamed creation and explicit mutations by readback. Preserve independent verifier restorations and their primary failures. Refs #5475. Focused bridge regressions and the complete real-Hopper suite passed on macOS arm64 with Hopper 6.6.0-demo 42.6316032. The verifier used a temporary unavailable Ghidra override to meet its sole-candidate prerequisite; reporter Hopper 6.1 remains unverified.
185 lines
6.5 KiB
Python
185 lines
6.5 KiB
Python
"""Exercise bookmark mutation and exact readback at the Hopper bridge boundary."""
|
|
|
|
import json
|
|
from pathlib import Path
|
|
import sys
|
|
|
|
|
|
class FakeSegment:
|
|
pass
|
|
|
|
|
|
class FakeDocument:
|
|
def __init__(self):
|
|
self.names = {}
|
|
self.setter_calls = []
|
|
self.rename_calls = []
|
|
self.ignore_next_set = False
|
|
self.ignore_next_rename = False
|
|
self.default_name_on_omitted = False
|
|
|
|
def getDocumentName(self):
|
|
return "bookmark-fixture"
|
|
|
|
def getExecutableFilePath(self):
|
|
return "/tmp/rea-hopper-bookmark-fixture"
|
|
|
|
def getDatabaseFilePath(self):
|
|
return None
|
|
|
|
def backgroundProcessActive(self):
|
|
return False
|
|
|
|
def getSegmentAtAddress(self, address):
|
|
return FakeSegment() if address == 0x401000 else None
|
|
|
|
def setBookmarkAtAddress(self, address, name=None):
|
|
self.setter_calls.append((address, name))
|
|
if self.ignore_next_set:
|
|
self.ignore_next_set = False
|
|
return True
|
|
if address not in self.names:
|
|
if name is None and self.default_name_on_omitted:
|
|
self.names[address] = "provider-default"
|
|
else:
|
|
self.names[address] = "" if name == "" else name
|
|
elif name is not None and name != "":
|
|
self.names[address] = name
|
|
# The native API return is not authoritative; the bridge reads state back.
|
|
return False
|
|
|
|
def renameBookmarkAtAddress(self, address, name):
|
|
self.rename_calls.append((address, name))
|
|
if self.ignore_next_rename:
|
|
self.ignore_next_rename = False
|
|
return False
|
|
self.names[address] = None if name == "" else name
|
|
return False
|
|
|
|
def hasBookmarkAtAddress(self, address):
|
|
return address in self.names
|
|
|
|
def getBookmarkName(self, address):
|
|
return self.names[address]
|
|
|
|
def getBookmarks(self):
|
|
return list(self.names)
|
|
|
|
def removeBookmarkAtAddress(self, address):
|
|
self.names.pop(address, None)
|
|
return False
|
|
|
|
|
|
class FakeDocumentProvider:
|
|
document = FakeDocument()
|
|
|
|
@classmethod
|
|
def getAllDocuments(cls):
|
|
return [cls.document]
|
|
|
|
@classmethod
|
|
def getCurrentDocument(cls):
|
|
return cls.document
|
|
|
|
|
|
def main():
|
|
bridge_path = sys.argv[1]
|
|
namespace = {"__file__": bridge_path, "__name__": "rea_hopper_bridge"}
|
|
source = Path(bridge_path).read_text(encoding="utf-8")
|
|
exec(compile(source, bridge_path, "exec"), namespace)
|
|
namespace["REA_TARGET_PATH"] = FakeDocumentProvider.document.getExecutableFilePath()
|
|
namespace["_configure_hopper_api"](FakeDocumentProvider)
|
|
namespace["_bind_session_document"]()
|
|
dispatch = namespace["_dispatch"]
|
|
address = "0x401000"
|
|
document = FakeDocumentProvider.document
|
|
|
|
fresh_empty_result = dispatch("set_bookmark", {"address": address, "name": ""})
|
|
fresh_empty_listing = dispatch("list_bookmarks", {})
|
|
fresh_empty_raw_name = document.getBookmarkName(0x401000)
|
|
dispatch("unset_bookmark", {"address": address})
|
|
|
|
document.default_name_on_omitted = True
|
|
default_named_omitted_result = dispatch("set_bookmark", {"address": address})
|
|
default_named_omitted_listing = dispatch("list_bookmarks", {})
|
|
dispatch("unset_bookmark", {"address": address})
|
|
document.default_name_on_omitted = False
|
|
|
|
fresh_omitted_result = dispatch("set_bookmark", {"address": address})
|
|
fresh_omitted_listing = dispatch("list_bookmarks", {})
|
|
dispatch("unset_bookmark", {"address": address})
|
|
|
|
named_creation_result = dispatch(
|
|
"set_bookmark", {"address": address, "name": "initial-name"}
|
|
)
|
|
clear_result = dispatch("set_bookmark", {"address": address, "name": ""})
|
|
clear_listing = dispatch("list_bookmarks", {})
|
|
renamed_result = dispatch(
|
|
"set_bookmark", {"address": address, "name": "replacement-name"}
|
|
)
|
|
replacement_listing = dispatch("list_bookmarks", {})
|
|
setter_calls_before_omitted = len(document.setter_calls)
|
|
omitted_result = dispatch("set_bookmark", {"address": address})
|
|
setter_calls_after_omitted = len(document.setter_calls)
|
|
omitted_listing = dispatch("list_bookmarks", {})
|
|
|
|
document.ignore_next_set = True
|
|
ignored_set_result = dispatch(
|
|
"set_bookmark", {"address": address, "name": "ignored-name"}
|
|
)
|
|
ignored_set_listing = dispatch("list_bookmarks", {})
|
|
|
|
document.ignore_next_rename = True
|
|
failed_clear_result = dispatch("set_bookmark", {"address": address, "name": ""})
|
|
failed_clear_listing = dispatch("list_bookmarks", {})
|
|
|
|
rename_method = document.renameBookmarkAtAddress
|
|
document.renameBookmarkAtAddress = None
|
|
missing_rename = None
|
|
try:
|
|
dispatch("set_bookmark", {"address": address, "name": ""})
|
|
except Exception as error:
|
|
missing_rename = {
|
|
"type": type(error).__name__,
|
|
"diagnostic_type": namespace["_diagnostic_type"](error),
|
|
}
|
|
document.renameBookmarkAtAddress = rename_method
|
|
preserved_after_missing_rename = dispatch("list_bookmarks", {})
|
|
unset_result = dispatch("unset_bookmark", {"address": address})
|
|
after_unset = dispatch("list_bookmarks", {})
|
|
|
|
print(json.dumps({
|
|
"fresh_empty": {
|
|
"result": fresh_empty_result,
|
|
"listing": fresh_empty_listing,
|
|
"raw_name": fresh_empty_raw_name,
|
|
},
|
|
"fresh_omitted": {
|
|
"result": fresh_omitted_result,
|
|
"listing": fresh_omitted_listing,
|
|
},
|
|
"default_named_omitted": {
|
|
"result": default_named_omitted_result,
|
|
"listing": default_named_omitted_listing,
|
|
},
|
|
"named_creation_result": named_creation_result,
|
|
"empty_clear": {"result": clear_result, "listing": clear_listing},
|
|
"nonempty_replacement": {"result": renamed_result, "listing": replacement_listing},
|
|
"omitted_existing": {
|
|
"result": omitted_result,
|
|
"setter_calls_before": setter_calls_before_omitted,
|
|
"setter_calls_after": setter_calls_after_omitted,
|
|
"listing": omitted_listing,
|
|
},
|
|
"ignored_setter": {"result": ignored_set_result, "listing": ignored_set_listing},
|
|
"failed_clear": {"result": failed_clear_result, "listing": failed_clear_listing},
|
|
"missing_rename": missing_rename,
|
|
"preserved_after_missing_rename": preserved_after_missing_rename,
|
|
"unset": {"result": unset_result, "listing": after_unset},
|
|
"rename_calls": [[hex(item[0]), item[1]] for item in document.rename_calls],
|
|
}, sort_keys=True))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|