mirror of
https://github.com/logos-co/logos-protocol.git
synced 2026-08-27 12:01:15 +00:00
* fix(codec): signedness and range are part of the integer type Codec<T>::from accepted any integral JSON number and handed it to .get<T>(). That is silent in both directions: .get<uint64_t>() on -1 -> 18446744073709551615 (a sign flip) .get<int32_t>() on 2^40 -> truncated Both now reject with the usual path-carrying CodecError instead. Rejecting is the codec's existing contract — a value the declared type cannot represent must not reach business logic wearing a different one — this just extends it to the half of the integer domain it was skipping. Note the check is on the JSON category, not the value: a negative literal parses as number_integer and never as number_unsigned, so `is_number_unsigned()` is the reliable discriminator rather than a comparison after conversion. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(async): the pending-call sentinel is matched by shape, not by key presence All four detection sites tested `m.contains(pendingCallKey())` and nothing else, so ANY user map carrying that key was taken for a deferred call: the consumer extracted a call id, found no completion, and waited out a nested event loop. The measured outcome is a ~20s HANG, not a fast failure. An `any` slot is enough to reach it — anything a user can put in a map. logos::isPendingCallSentinel now requires the canonical shape: exactly one entry, under the sentinel key, holding a non-empty string. Shape and signature are mirrored from isUnauthorizedSentinel (logos_rpc_status.h), QJsonObject arm included — the two are the same kind of in-band marker and there was no reason for them to be guarded differently. That guard, and isTaggedBytes's, both already existed in this repo; the difference was chronology, not principle. Behaviour-preserving: the generated glue builds this map with exactly one entry whose value is a QString call id, so no real sender changes. The concurrent dispatch tests pass unchanged. NARROWS, DOES NOT CLOSE — and the tests say so out loud. A one-key, string-valued forgery IS the sentinel; no predicate can separate them. It still hangs, and because call ids are a per-object counter from 0, a forged "lc-0" can collide with a genuine in-flight completion and steal its result. Closing that needs an out-of-band channel for "deferred", which the single-QVariant dispatch slot cannot express without an ABI break — the constraint is stated at logos_rpc_status.h:24-27 and is real. tests: 10 new, including one asserting the forgery still matches, so a future reader cannot mistake the green cells for "the sentinel is safe". 236/236. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
84 lines
3.7 KiB
C++
84 lines
3.7 KiB
C++
#ifndef LOGOS_ASYNC_DISPATCH_H
|
|
#define LOGOS_ASYNC_DISPATCH_H
|
|
|
|
#include <QJsonObject>
|
|
#include <QMetaType>
|
|
#include <QString>
|
|
#include <QVariant>
|
|
#include <QVariantMap>
|
|
|
|
// Shared wire constants for "multi" (concurrent) dispatch. Concurrency is a
|
|
// MODULE-side concern handled entirely behind the ordinary callMethod entry
|
|
// point — no new provider/host vtable method, so the provider ABI is unchanged
|
|
// and an old host/daemon loads a "multi" module and forwards its traffic
|
|
// without even understanding these markers.
|
|
//
|
|
// A "multi" module's generated glue does NOT block in callMethod: it hands the
|
|
// handler to a worker and returns a PENDING SENTINEL immediately (a QVariantMap
|
|
// carrying the call id under pendingCallKey()). When the worker finishes, the
|
|
// module pushes the real result back as a COMPLETION event
|
|
// (callCompleteEvent(), data = [callId, result]) over the SAME event channel it
|
|
// already uses (setEventListener). The host (ModuleProxy / liblogos) is a pure
|
|
// forwarder — it returns whatever callMethod returned and forwards whatever
|
|
// events the module emits. The CONSUMER transport (RemoteLogosObject for QtRO,
|
|
// PlainLogosObject for the plain transport) detects the sentinel, waits for the
|
|
// matching completion keyed by callId, and returns the real result — so
|
|
// generated clients call transparently. Both transports use this path; the
|
|
// version that speaks it is logos-protocol 0.2 (additive minor — see
|
|
// logos_protocol.h).
|
|
namespace logos {
|
|
|
|
inline QString pendingCallKey() { return QStringLiteral("__logos_pending_call__"); }
|
|
inline QString callCompleteEvent() { return QStringLiteral("__logos_call_complete__"); }
|
|
|
|
// True only for the exact pending-call sentinel, writing the call id to `callId`.
|
|
//
|
|
// The four detection sites used to test `m.contains(pendingCallKey())` with no
|
|
// shape check at all, so ANY user map that happened to carry that key was taken
|
|
// for a deferred call: the consumer then waited in a nested event loop for a
|
|
// completion that never arrives, and the call hung for the full timeout. An
|
|
// `any` slot is enough to reach it, i.e. anything a user can put in a map.
|
|
//
|
|
// The generated glue builds this map with exactly one entry whose value is a
|
|
// QString call id, so the checks below are behaviour-preserving for every real
|
|
// sender. Shape is mirrored from isUnauthorizedSentinel (logos_rpc_status.h),
|
|
// including the QJsonObject arm — the two are the same kind of in-band marker
|
|
// and there is no reason for them to be guarded differently.
|
|
//
|
|
// NARROWS, DOES NOT CLOSE. A forgery of the canonical shape — one key, string
|
|
// value — is still indistinguishable from the real thing, because that IS the
|
|
// real thing. Closing it needs an out-of-band channel for "deferred", which the
|
|
// single-QVariant dispatch slot cannot express without an ABI break.
|
|
inline bool isPendingCallSentinel(const QVariant& v, QString* callId = nullptr)
|
|
{
|
|
const QString key = pendingCallKey();
|
|
QString id;
|
|
switch (v.userType()) {
|
|
case QMetaType::QVariantMap: {
|
|
const QVariantMap m = v.toMap();
|
|
if (m.size() != 1) return false;
|
|
const QVariant held = m.value(key);
|
|
if (held.userType() != QMetaType::QString) return false;
|
|
id = held.toString();
|
|
break;
|
|
}
|
|
case QMetaType::QJsonObject: {
|
|
const QJsonObject o = v.toJsonObject();
|
|
if (o.size() != 1) return false;
|
|
const QJsonValue held = o.value(key);
|
|
if (!held.isString()) return false;
|
|
id = held.toString();
|
|
break;
|
|
}
|
|
default:
|
|
return false;
|
|
}
|
|
if (id.isEmpty()) return false;
|
|
if (callId) *callId = id;
|
|
return true;
|
|
}
|
|
|
|
} // namespace logos
|
|
|
|
#endif // LOGOS_ASYNC_DISPATCH_H
|