From 261ee8b69ed143f050b30ced3ab28a6dfc8d61c3 Mon Sep 17 00:00:00 2001 From: Mario Fetka Date: Thu, 23 Jul 2026 14:13:07 +0200 Subject: [PATCH] Fix store property backup ownership --- src/agents/store/object-model.c | 17 +++++++--- .../bongo/storetool/CalendarCommands.py | 22 ++++++------- .../storetool/tests/test_python3_runtime.py | 33 +++++++++++++++++-- src/libs/python/bongo/store/StoreClient.py | 23 ++++++------- 4 files changed, 65 insertions(+), 30 deletions(-) diff --git a/src/agents/store/object-model.c b/src/agents/store/object-model.c index 4a72ee2..e1ac6dd 100644 --- a/src/agents/store/object-model.c +++ b/src/agents/store/object-model.c @@ -1066,17 +1066,26 @@ StoreObjectIterProperties(StoreClient *client, StoreObject *document) while ((ccode = MsgSQLResults(client->storedb, &stmt)) > 0) { StorePropInfo prop; + char *storedName = NULL; + char *storedValue = NULL; memset(&prop, 0, sizeof(StorePropInfo)); prop.type = MsgSQLResultInt(&stmt, 0); - MsgSQLResultTextPtr(&stmt, 1, &prop.name); - MsgSQLResultTextPtr(&stmt, 2, &prop.value); + MsgSQLResultTextPtr(&stmt, 1, &storedName); + MsgSQLResultTextPtr(&stmt, 2, &storedValue); + prop.name = storedName; + prop.value = storedValue; + /* + * Known integer properties are stored without a name. The fixup + * replaces that NULL pointer with a static StorePropTable string, + * so retain the database-owned pointers for cleanup. + */ StorePropertyFixup(&prop); StoreOutputProperty(client, prop.type, prop.name, prop.value); - MemFree(prop.name); - MemFree(prop.value); + MemFree(storedName); + MemFree(storedValue); } MsgSQLFinalize(&stmt); diff --git a/src/apps/storetool/bongo/storetool/CalendarCommands.py b/src/apps/storetool/bongo/storetool/CalendarCommands.py index 26d8580..780508e 100644 --- a/src/apps/storetool/bongo/storetool/CalendarCommands.py +++ b/src/apps/storetool/bongo/storetool/CalendarCommands.py @@ -51,6 +51,14 @@ def _event_filename(event): return "import-" + hashlib.sha256(uid).hexdigest() +def _json_component_value(component, *names): + for name in names: + field = component.get(name) + if isinstance(field, dict): + return field.get("value") + return None + + class CalendarsCommand(Command): log = logging.getLogger("Bongo.StoreTool") @@ -108,17 +116,9 @@ class CalendarEventsCommand(Command): jsob = simplejson.loads(event.props["nmap.document"].strip()) comp = jsob["components"][0] - summary = comp.get("summary") - if summary is not None: - summary = summary.get("value") - - start = comp.get("start") - if start is not None: - start = start.get("value") - - end = comp.get("end") - if end is not None: - end = end.get("value") + summary = _json_component_value(comp, "summary") + start = _json_component_value(comp, "dtstart", "start") + end = _json_component_value(comp, "dtend", "end") rows.append((summary, start, end)) finally: diff --git a/src/apps/storetool/tests/test_python3_runtime.py b/src/apps/storetool/tests/test_python3_runtime.py index b2e6cfc..9bd6975 100644 --- a/src/apps/storetool/tests/test_python3_runtime.py +++ b/src/apps/storetool/tests/test_python3_runtime.py @@ -32,7 +32,8 @@ from bongo.external.simpletal import simpleTAL, simpleTALES from bongo.store.StoreClient import CalendarACL, StoreClient from bongo.storetool.CalendarCommands import (_collection_name, _event_filename, - _event_payload) + _event_payload, + _json_component_value) from bongo.storetool.Connection import connect_store from bongo.storetool.ContactCommands import AddressbookContactsCommand from bongo.storetool.ExportMailbox import MaildirMailbox, MboxMailbox @@ -45,8 +46,9 @@ class _Response: class _Stream: - def __init__(self, responses): + def __init__(self, responses, reads=None): self.responses = list(responses) + self.reads = list(reads or []) self.lines = [] self.raw = [] @@ -59,6 +61,11 @@ class _Stream: def GetResponse(self): return self.responses.pop(0) + def Read(self, length): + value = self.reads.pop(0) + self.assert_read_length = length + return value + class Python3RuntimeTests(unittest.TestCase): @mock.patch("bongo.storetool.Connection.StoreClient") @@ -89,6 +96,13 @@ class Python3RuntimeTests(unittest.TestCase): self.assertIn("BEGIN:VEVENT", payload) self.assertIn("UID:event@example.test", payload) self.assertRegex(_event_filename(event), r"^import-[0-9a-f]{64}$") + component = { + "dtstart": {"value": "20260724T080000Z"}, + "start": {"value": "legacy"}, + } + self.assertEqual( + _json_component_value(component, "dtstart", "start"), + "20260724T080000Z") def test_contact_listing_reads_document_bodies(self): addressbook = SimpleNamespace(uid="addressbook-guid") @@ -151,6 +165,21 @@ class Python3RuntimeTests(unittest.TestCase): client.stream = _Stream([_Response(4224, "Document doesn't exist")]) self.assertIsNone(client.Info("/events/new-event")) + def test_store_propget_does_not_request_imap_uid_for_non_mail(self): + client = object.__new__(StoreClient) + client.stream = _Stream( + [_Response(2001, "nmap.type 1"), _Response(1000)], + [b"3", b"\r\n"]) + + self.assertEqual(client.PropGet("event-guid"), {"nmap.type": "3"}) + self.assertEqual(client.stream.lines, ["PROPGET event-guid"]) + + def test_store_propget_ignores_missing_optional_property_response(self): + client = object.__new__(StoreClient) + client.stream = _Stream([_Response(3245), _Response(1000)]) + + self.assertEqual(client.PropGet("event-guid"), {}) + def test_store_write_links_event_after_successful_write(self): client = object.__new__(StoreClient) client.stream = _Stream([ diff --git a/src/libs/python/bongo/store/StoreClient.py b/src/libs/python/bongo/store/StoreClient.py index a10f341..9d866ba 100644 --- a/src/libs/python/bongo/store/StoreClient.py +++ b/src/libs/python/bongo/store/StoreClient.py @@ -707,8 +707,8 @@ class StoreClient: r = self.stream.GetResponse() while r.code == 2001 or r.code == 3245: if r.code == 3245: - # set empty properties to None - props[key] = None + # A missing optional property has no value to consume. In + # particular, do not reuse the key from a previous response. r = self.stream.GetResponse() continue @@ -721,17 +721,14 @@ class StoreClient: # eat the \r\n afterward self.stream.Read(2) r = self.stream.GetResponse() - - # automatically pull out imap_uid from Bongo 0.3. - if name is None and "nmap.mail.imapuid" not in props: - self.stream.Write("PROPGET %s nmap.mail.imapuid" % (doc)) - r = self.stream.GetResponse() - if r.code != 2001: - raise CommandError(r) - (key, length) = r.message.split(" ", 2) - raw_data = self.stream.Read(int(length)) - self.stream.Read(2) - props["nmap.mail.imapuid"] = self.force_utf8(raw_data) + + if r.code != 1000: + raise CommandError(r) + + # Current stores include nmap.mail.imapuid in the complete property + # response for mail and folder objects. Contacts and events do not + # have an IMAP UID, so the old Bongo 0.3 compatibility query must not + # be issued for every document. return props def PropSet(self, doc, name, value=None):