diff options
| -rw-r--r-- | src/m_email/EMailAccountStore.cpp | 3 | ||||
| -rw-r--r-- | src/m_email/EMailBasicGpgOpera.cpp | 68 | ||||
| -rw-r--r-- | src/m_email/EMailCredentialStore.cpp | 43 | ||||
| -rw-r--r-- | src/m_email/EMailModule.cpp | 100 | ||||
| -rw-r--r-- | src/m_email/EMailPageView.cpp | 79 | ||||
| -rw-r--r-- | src/m_email/EMailSecret.cpp | 22 | ||||
| -rw-r--r-- | src/m_email/EMailSecret.h | 17 | ||||
| -rw-r--r-- | src/m_email/EMailSecurityView.cpp | 32 | ||||
| -rw-r--r-- | src/m_email/EMailStructureView.cpp | 6 | ||||
| -rw-r--r-- | src/m_email/EMailViewStyle.h | 29 | ||||
| -rw-r--r-- | src/m_email/module.json | 5 | ||||
| -rw-r--r-- | src/m_email/test/CMakeLists.txt | 15 | ||||
| -rw-r--r-- | src/m_email/test/EMailCryptoSdkStubs.cpp | 270 | ||||
| -rw-r--r-- | src/m_email/test/EMailSdkStubs.cpp | 295 | ||||
| -rw-r--r-- | src/m_email/test/EMailSecretTest.cpp | 28 |
15 files changed, 682 insertions, 330 deletions
diff --git a/src/m_email/EMailAccountStore.cpp b/src/m_email/EMailAccountStore.cpp index 0198205..7216bac 100644 --- a/src/m_email/EMailAccountStore.cpp +++ b/src/m_email/EMailAccountStore.cpp @@ -50,7 +50,8 @@ constexpr auto kSchemaVersion = 1; Q_GLOBAL_STATIC(QMutex, settings_mutex) auto GlobalSettings() -> QSettings* { - return qobject_cast<QSettings*>(static_cast<QObject*>(GFUIGlobalSettings())); + return qobject_cast<QSettings*>( + static_cast<QObject*>(GFStorageSettingsRoot(GFModuleSdkContext()))); } } // namespace diff --git a/src/m_email/EMailBasicGpgOpera.cpp b/src/m_email/EMailBasicGpgOpera.cpp index e0154a2..f1ceace 100644 --- a/src/m_email/EMailBasicGpgOpera.cpp +++ b/src/m_email/EMailBasicGpgOpera.cpp @@ -111,11 +111,11 @@ auto EncryptPlainText(int channel, const QStringList& keys, // including the early return below, which is where the struct-based API // leaked because a FAILED call still allocated a result to explain // itself. - auto in = GFBuf::Copy(body_data); + auto in = GFBuf::Copy(GFModuleSdkContext(), body_data); auto key_ids = KeyIdArgs(keys); - GFGpgResult r; - if (GFGpgEncrypt(channel, key_ids.Data(), key_ids.Size(), in.View(), 1, - r.Out()) != GF_GPG_OK) { + GFGpgResult r(GFModuleSdkContext()); + if (GFGpgEncrypt(GFModuleSdkContext(), channel, key_ids.Data(), + key_ids.Size(), in.View(), 1, r.Out()) != GF_GPG_OK) { eml_data = SdkFailureText("Encryption", r.ErrorString()).toUtf8(); return kFAILED; } @@ -286,11 +286,11 @@ auto EncryptEMLData(int channel, const QStringList& keys, // The SDK sets *ps = nullptr and returns non-zero when it cannot // allocate the result, so both must be checked before the first // dereference below -- not after it, as this used to. - auto in = GFBuf::Copy(plain_raw_data); + auto in = GFBuf::Copy(GFModuleSdkContext(), plain_raw_data); auto key_ids = KeyIdArgs(keys); - GFGpgResult r; - if (GFGpgEncrypt(channel, key_ids.Data(), key_ids.Size(), in.View(), 1, - r.Out()) != GF_GPG_OK) { + GFGpgResult r(GFModuleSdkContext()); + if (GFGpgEncrypt(GFModuleSdkContext(), channel, key_ids.Data(), + key_ids.Size(), in.View(), 1, r.Out()) != GF_GPG_OK) { eml_data = SdkFailureText("Encryption", r.ErrorString()).toUtf8(); return kFAILED; } @@ -542,7 +542,7 @@ auto SignPlainText(int channel, const QString& key, vmime::word(std::string{"OpenPGP_signature.asc"})); auto public_key = - UDUP(GFGpgPublicKey(channel, (key).toUtf8().constData(), 1)); + UDUP(gf::sdk::PublicKey(GFModuleSdkContext(), channel, (key), 1 != 0)); if (public_key.isEmpty()) { eml_data = "Get Public Key of Sign Key Failed"; return kFAILED; @@ -593,11 +593,11 @@ auto SignPlainText(int channel, const QString& key, // The SDK sets *ps = nullptr and returns non-zero when it cannot // allocate the result, so both must be checked before the first // dereference below -- not after it, as this used to. - auto in = GFBuf::Copy(container_raw_data); + auto in = GFBuf::Copy(GFModuleSdkContext(), container_raw_data); auto key_ids = KeyIdArgs({key}); - GFGpgResult r; - if (GFGpgSign(channel, key_ids.Data(), key_ids.Size(), in.View(), 1, 1, - r.Out()) != GF_GPG_OK) { + GFGpgResult r(GFModuleSdkContext()); + if (GFGpgSign(GFModuleSdkContext(), channel, key_ids.Data(), key_ids.Size(), + in.View(), 1, 1, r.Out()) != GF_GPG_OK) { eml_data = SdkFailureText("Sign", r.ErrorString()).toUtf8(); return kFAILED; } @@ -777,7 +777,7 @@ auto SignEMLData(int channel, const QString& key, vmime::word(std::string{"OpenPGP_signature.asc"})); auto public_key = - UDUP(GFGpgPublicKey(channel, (key).toUtf8().constData(), 1)); + UDUP(gf::sdk::PublicKey(GFModuleSdkContext(), channel, (key), 1 != 0)); if (public_key.isEmpty()) { eml_data = "Get Public Key of Sign Key Failed"; return kFAILED; @@ -827,11 +827,11 @@ auto SignEMLData(int channel, const QString& key, // The SDK sets *ps = nullptr and returns non-zero when it cannot // allocate the result, so both must be checked before the first // dereference below -- not after it, as this used to. - auto in = GFBuf::Copy(container_raw_data); + auto in = GFBuf::Copy(GFModuleSdkContext(), container_raw_data); auto key_ids = KeyIdArgs({key}); - GFGpgResult r; - if (GFGpgSign(channel, key_ids.Data(), key_ids.Size(), in.View(), 1, 1, - r.Out()) != GF_GPG_OK) { + GFGpgResult r(GFModuleSdkContext()); + if (GFGpgSign(GFModuleSdkContext(), channel, key_ids.Data(), key_ids.Size(), + in.View(), 1, 1, r.Out()) != GF_GPG_OK) { eml_data = SdkFailureText("Sign", r.ErrorString()).toUtf8(); return kFAILED; } @@ -1141,10 +1141,11 @@ auto VerifyOneRegion(int channel, const QByteArray& raw, // It EXPLAINS a failure below; it never rescues one. verdict.signed_bytes_non_canonical = HasBareLineFeeds(signed_bytes); - auto in = GFBuf::Copy(signed_bytes); - auto sig = GFBuf::Copy(signature_bytes); - GFGpgResult r; - if (GFGpgVerify(channel, in.View(), sig.View(), r.Out()) != GF_GPG_OK) { + auto in = GFBuf::Copy(GFModuleSdkContext(), signed_bytes); + auto sig = GFBuf::Copy(GFModuleSdkContext(), signature_bytes); + GFGpgResult r(GFModuleSdkContext()); + if (GFGpgVerify(GFModuleSdkContext(), channel, in.View(), sig.View(), + r.Out()) != GF_GPG_OK) { // One region failing is not the walk failing: the others are still worth // verifying. Nothing to reclaim by hand -- r releases itself. verdict.exec = EMailVerifyExec::kENGINE_ERROR; @@ -1162,16 +1163,14 @@ auto VerifyOneRegion(int channel, const QByteArray& raw, // The structured form AND the report text, from the one call: a per- // signature view cannot be rebuilt by re-reading prose, and the capsule is // consumed here, so everything anyone needs has to come out of it now. - const char* analyse = nullptr; - const char* cards = nullptr; - const char* info_json = nullptr; report.region_id = region.region_id; - report.status = GFAnalyseVerifyResultInfoByCapsule( - channel, err, (capsule_id).toUtf8().constData(), &analyse, &cards, - &info_json); - report.detail = UnStrDup(analyse); - report.cards = UnStrDup(cards); - report.info_json = UnStrDup(info_json).toUtf8(); + + const auto analysis = gf::sdk::AnalyseResult( + GFModuleSdkContext(), GF_GPG_ANALYSE_VERIFY, channel, err, capsule_id); + report.status = analysis.status; + report.detail = analysis.report; + report.cards = analysis.cards; + report.info_json = analysis.info_json.toUtf8(); auto region_results = ParseSignatureResults(report.info_json, region.region_id); @@ -1465,9 +1464,10 @@ auto DecryptEMLData(int channel, const QByteArray& data, // The SDK sets *ps = nullptr and returns non-zero when it cannot // allocate the result, so both must be checked before the first // dereference below -- not after it, as this used to. - auto in = GFBuf::Copy(part_encr_body_content); - GFGpgResult r; - if (GFGpgDecrypt(channel, in.View(), r.Out()) != GF_GPG_OK) { + auto in = GFBuf::Copy(GFModuleSdkContext(), part_encr_body_content); + GFGpgResult r(GFModuleSdkContext()); + if (GFGpgDecrypt(GFModuleSdkContext(), channel, in.View(), r.Out()) != + GF_GPG_OK) { eml_data = SdkFailureText("Decrypt", r.ErrorString()).toUtf8(); return kFAILED; } diff --git a/src/m_email/EMailCredentialStore.cpp b/src/m_email/EMailCredentialStore.cpp index 6208dae..6aefd51 100644 --- a/src/m_email/EMailCredentialStore.cpp +++ b/src/m_email/EMailCredentialStore.cpp @@ -29,7 +29,6 @@ #include "EMailCredentialStore.h" #include "GFModule.h" -#include "GFSDKBasic.h" namespace { @@ -44,7 +43,7 @@ auto CredentialKey(const QString& account_id) -> QString { namespace EMailCredentialStore { auto CurrentProtection() -> ProtectionLevel { - switch (GFAppKeyProtectionLevel()) { + switch (GFAppKeyProtectionLevel(GFModuleSdkContext())) { case 0: return ProtectionLevel::kNONE; case 1: @@ -88,14 +87,24 @@ auto Save(const QString& account_id, const EMailSecret& password) -> bool { // Both arguments are handed over owned, from the allocators the SDK will // release them through: the key ordinary, the secret secure. Passing a // QByteArray's internal pointer here would have the SDK free memory Qt owns. - auto* secret = password.ToSecureCString(); - if (secret == nullptr) { + // Deliberately the raw buffer API rather than the QString convenience: a + // QString cannot be erased, and routing a password through one would add a + // copy this module could never wipe. The buffer is wiped on release. + // + // The secure tier BORROWS now, like every other argument in this SDK. It + // used to take ownership of both arguments and free them through two + // different allocators, a rule nothing in the signature hinted at. + auto* ctx = GFModuleSdkContext(); + auto* buffer = GFBufferNewFromBytes(ctx, password.Data(), password.Size()); + if (buffer == nullptr) { LOG_ERROR("could not allocate a secure buffer for a mail credential"); return false; } - const auto result = GFSecDurableCacheSave( - (CredentialKey(account_id)).toUtf8().constData(), secret); + const auto result = GFStorageCacheSet( + ctx, GF_STORE_SECURE_DURABLE, + CredentialKey(account_id).toUtf8().constData(), buffer, 0); + GFBufferRelease(ctx, buffer); if (result != 0) LOG_ERROR("failed to store mail credential"); return result == 0; @@ -104,15 +113,18 @@ auto Save(const QString& account_id, const EMailSecret& password) -> bool { auto Load(const QString& account_id) -> EMailSecretPtr { if (account_id.isEmpty()) return std::make_shared<EMailSecret>(); - auto* raw = - GFSecDurableCacheGet((CredentialKey(account_id)).toUtf8().constData()); - if (raw == nullptr) return std::make_shared<EMailSecret>(); + // Raw buffer again, for the same reason: the secret must not pass through + // a QString on its way into storage that can erase it. + auto* ctx = GFModuleSdkContext(); + GFBufferRef raw = nullptr; + if (GFStorageCacheGet(ctx, GF_STORE_SECURE_DURABLE, + CredentialKey(account_id).toUtf8().constData(), + &raw) != 0) { + return std::make_shared<EMailSecret>(); + } - // AdoptCString copies the bytes out, wipes the SDK's buffer and frees it to - // the secure allocator it came from, so the secret exists in exactly one - // place from here on -- and that place can actually be erased, which a - // QString could not be. See EMailSecret. - return EMailSecret::AdoptCString(raw); + // AdoptBuffer copies the bytes out and releases the buffer, which wipes it. + return EMailSecret::AdoptBuffer(ctx, raw); } auto Has(const QString& account_id) -> bool { @@ -121,7 +133,8 @@ auto Has(const QString& account_id) -> bool { void Remove(const QString& account_id) { if (account_id.isEmpty()) return; - GFSecDurableCacheRemove((CredentialKey(account_id)).toUtf8().constData()); + gf::sdk::RemoveCache(GFModuleSdkContext(), GF_STORE_SECURE_DURABLE, + (CredentialKey(account_id)).toUtf8().constData()); } } // namespace EMailCredentialStore diff --git a/src/m_email/EMailModule.cpp b/src/m_email/EMailModule.cpp index a1b8049..309fe8e 100644 --- a/src/m_email/EMailModule.cpp +++ b/src/m_email/EMailModule.cpp @@ -28,7 +28,6 @@ #include "EMailModule.h" -#include <GFSDKBasic.h> #include <GFSDKBuildInfo.h> #include <GFSDKLog.h> @@ -168,16 +167,19 @@ auto BuildAttachmentCard(const EMailMetaData& m) -> QJsonObject { auto AddressOfKey(int channel, const QString& key_id) -> QString { if (key_id.isEmpty()) return {}; - GFGpgKeyUID* uid = nullptr; - if (GFGpgKeyPrimaryUID(channel, (key_id).toUtf8().constData(), &uid) != 0 || - uid == nullptr) { + // Three out-parameters, and only the two that are wanted. The struct this + // replaced handed back all three as char* members to be freed one by one, + // which is why the comment field used to be fetched purely to release it. + auto* ctx = GFModuleSdkContext(); + GFBufferRef name_buf = nullptr; + GFBufferRef email_buf = nullptr; + if (GFGpgKeyPrimaryUid(ctx, channel, key_id.toUtf8().constData(), &name_buf, + &email_buf, nullptr) != 0) { return {}; } - const auto name = UDUP(uid->name); - const auto email = UDUP(uid->email); - UDUP(uid->comment); // free the unused field - GFFreeMemory(uid); + const auto name = gf::sdk::TakeString(ctx, name_buf); + const auto email = gf::sdk::TakeString(ctx, email_buf); return email.isEmpty() ? name : QString("%1 <%2>").arg(name, email); } @@ -402,12 +404,12 @@ auto OnActivate() -> GFResult { // The message view of an e-mail tab. The host still owns the page and its // document -- this only supplies the widget shown on top of it, with the raw // MIME still one click away. - GFUIRegisterTabPageView( - "EMAIL", [](void*) -> void* { return new EMailPageView(nullptr); }, - nullptr); + gf::sdk::RegisterTabPageView( + GFModuleSdkContext(), "EMAIL", + [](void*) -> void* { return new EMailPageView(nullptr); }, nullptr); // register file extension handler - GFUIRegisterFileExtensionHandleEvent("eml", "EMAIL"); + GFUIRegisterFileExtension(GFModuleSdkContext(), "eml", "EMAIL"); // These cross thread boundaries as queued signal arguments, so Qt has to // know how to copy them before the first connection is made. qRegisterMetaType<MailAccountConfig>("MailAccountConfig"); @@ -436,10 +438,10 @@ auto OnActivate() -> GFResult { QStringList{GC_TR("mail"), GC_TR("email"), GC_TR("imap"), GC_TR("smtp"), GC_TR("account"), GC_TR("send")} .join('\n'); - GFUIRegisterSettingsPage(kMailSettingsPageId, "features", - GC_TR("Mail Accounts"), - (keywords).toUtf8().constData(), - EMailAccountSettingsPageFactory, nullptr); + gf::sdk::RegisterSettingsPage(GFModuleSdkContext(), kMailSettingsPageId, + "features", GC_TR("Mail Accounts"), + keywords.toUtf8().constData(), + EMailAccountSettingsPageFactory, nullptr); return GFResult::Ok(); } @@ -447,8 +449,8 @@ auto OnActivate() -> GFResult { auto OnDeactivate() -> GFResult { // A factory pointing into an unloaded shared object would crash the next // time an e-mail tab is opened. - GFUIUnregisterTabPageView("EMAIL"); - GFUIUnregisterSettingsPage(kMailSettingsPageId); + GFUIUnregisterTabPageView(GFModuleSdkContext(), "EMAIL"); + GFUIUnregisterSettingsPage(GFModuleSdkContext(), kMailSettingsPageId); return GFResult::Ok(); } @@ -522,7 +524,7 @@ auto ConfirmExport(QWidget* parent, const EMailExportCheck& check) -> bool { // Where a Save dialog should open. Falls back to the home directory only if // the host cannot answer, which it always can in practice. auto default_save_dir() -> QString { - auto path = UnStrDup(GFUIDefaultUserFilePath()); + const auto path = gf::sdk::UserFilePath(GFModuleSdkContext()); return path.isEmpty() ? QDir::homePath() : path; } @@ -1030,12 +1032,12 @@ auto DoDecryptEMLData(int channel, const QByteArray& data, const GFEvent& event, // The Info variant, because the recipient cross-check needs the structured // recipients rather than the rendered report. The capsule is consumed by // whichever analyse call touches it, so everything has to come from this one. - result_status = GFAnalyseDecryptResultInfoByCapsule( - channel, err, (capsule_id).toUtf8().constData(), &tmp, &cards_tmp, - &info_tmp); - result_detail = UnStrDup(tmp); - result_cards = UnStrDup(cards_tmp); - decrypt_info_json = UnStrDup(info_tmp).toUtf8(); + const auto analysis = gf::sdk::AnalyseResult( + GFModuleSdkContext(), GF_GPG_ANALYSE_DECRYPT, channel, err, capsule_id); + result_status = analysis.status; + result_detail = analysis.report; + result_cards = analysis.cards; + decrypt_info_json = analysis.info_json.toUtf8(); if (ret == kGPG_FAILED) { // decrypt failed. The analysis cards built above travel with it: a failure @@ -1161,12 +1163,12 @@ auto DoSignEMLData(int channel, const QString& sign_key, // The Info variant, not the plain one: the structured description // and details are what let a FAILURE explain itself, and without // them the board can only fall back to "<operation> failed." - result_status = GFAnalyseSignResultInfoByCapsule( - channel, err, (capsule_id).toUtf8().constData(), &tmp, &cards_tmp, - &info_tmp); - result_detail = UnStrDup(tmp); - result_cards = UnStrDup(cards_tmp); - info_json = UnStrDup(info_tmp).toUtf8(); + const auto analysis = gf::sdk::AnalyseResult( + GFModuleSdkContext(), GF_GPG_ANALYSE_SIGN, channel, err, capsule_id); + result_status = analysis.status; + result_detail = analysis.report; + result_cards = analysis.cards; + info_json = analysis.info_json.toUtf8(); if (ret == kGPG_FAILED) { // decrypt failed. The analysis cards built above travel with it: a failure @@ -1217,12 +1219,12 @@ auto DoSignPlainText(int channel, const QString& sign_key, // The Info variant, not the plain one: the structured description // and details are what let a FAILURE explain itself, and without // them the board can only fall back to "<operation> failed." - result_status = GFAnalyseSignResultInfoByCapsule( - channel, err, (capsule_id).toUtf8().constData(), &tmp, &cards_tmp, - &info_tmp); - result_detail = UnStrDup(tmp); - result_cards = UnStrDup(cards_tmp); - info_json = UnStrDup(info_tmp).toUtf8(); + const auto analysis = gf::sdk::AnalyseResult( + GFModuleSdkContext(), GF_GPG_ANALYSE_SIGN, channel, err, capsule_id); + result_status = analysis.status; + result_detail = analysis.report; + result_cards = analysis.cards; + info_json = analysis.info_json.toUtf8(); if (ret == kGPG_FAILED) { // decrypt failed. The analysis cards built above travel with it: a failure @@ -1339,12 +1341,12 @@ auto DoEncryptEMLData(int channel, const QStringList& encrypt_keys, // The Info variant, not the plain one: the structured description // and details are what let a FAILURE explain itself, and without // them the board can only fall back to "<operation> failed." - result_status = GFAnalyseEncryptResultInfoByCapsule( - channel, err, (capsule_id).toUtf8().constData(), &tmp, &cards_tmp, - &info_tmp); - result_detail = UnStrDup(tmp); - result_cards = UnStrDup(cards_tmp); - info_json = UnStrDup(info_tmp).toUtf8(); + const auto analysis = gf::sdk::AnalyseResult( + GFModuleSdkContext(), GF_GPG_ANALYSE_ENCRYPT, channel, err, capsule_id); + result_status = analysis.status; + result_detail = analysis.report; + result_cards = analysis.cards; + info_json = analysis.info_json.toUtf8(); if (ret == kGPG_FAILED) { // encrypt failed. The analysis cards built above travel with it: a failure @@ -1399,12 +1401,12 @@ auto DoEncryptPlainText(int channel, const QStringList& encrypt_keys, // The Info variant, not the plain one: the structured description // and details are what let a FAILURE explain itself, and without // them the board can only fall back to "<operation> failed." - result_status = GFAnalyseEncryptResultInfoByCapsule( - channel, err, (capsule_id).toUtf8().constData(), &tmp, &cards_tmp, - &info_tmp); - result_detail = UnStrDup(tmp); - result_cards = UnStrDup(cards_tmp); - info_json = UnStrDup(info_tmp).toUtf8(); + const auto analysis = gf::sdk::AnalyseResult( + GFModuleSdkContext(), GF_GPG_ANALYSE_ENCRYPT, channel, err, capsule_id); + result_status = analysis.status; + result_detail = analysis.report; + result_cards = analysis.cards; + info_json = analysis.info_json.toUtf8(); if (ret == kGPG_FAILED) { // encrypt failed. The analysis cards built above travel with it: a failure diff --git a/src/m_email/EMailPageView.cpp b/src/m_email/EMailPageView.cpp index 59b9e14..9330763 100644 --- a/src/m_email/EMailPageView.cpp +++ b/src/m_email/EMailPageView.cpp @@ -457,7 +457,7 @@ void EMailPageView::apply_colors() { EMailSetLabelColor(attachment_heading_, MutedColor(this)); } if (unsigned_notice_ != nullptr) { - EMailSetLabelColor(unsigned_notice_, ThemeColor(this, &GFUIWarningColor)); + EMailSetLabelColor(unsigned_notice_, ThemeColor(this, GF_UI_COLOR_WARNING)); } if (attachment_list_ != nullptr) { @@ -1936,11 +1936,12 @@ void EMailPageView::refresh_locked_capability(const QStringList& named) { // for mail encrypted to someone else who happened to share an address. const auto* ciphertext = FindEncryptedCiphertextPart(tree_root_); if (ciphertext != nullptr) { - auto in = GFBuf::Copy(ciphertext->data); + auto in = GFBuf::Copy(GFModuleSdkContext(), ciphertext->data); GFGpgRecipientListRef list = nullptr; - if (GFGpgSniffRecipients(GFGpgCurrentGpgContextChannel(), in.View(), - &list) == 0) { - const auto count = GFGpgRecipientListCount(list); + if (GFGpgSniffRecipients(GFModuleSdkContext(), + GFGpgCurrentChannel(GFModuleSdkContext()), + in.View(), &list) == 0) { + const auto count = GFGpgRecipientCount(GFModuleSdkContext(), list); QList<EMailEncRecipient> recipients; recipients.reserve(static_cast<qsizetype>(count)); for (size_t i = 0; i < count; ++i) { @@ -1948,16 +1949,20 @@ void EMailPageView::refresh_locked_capability(const QStringList& named) { // pointer that dies with the list. There is no per-element free to // get wrong, and no count to hand back on release. recipients.append(EMailEncRecipient{ - QString::fromUtf8(GFGpgRecipientKeyId(list, i)), - QString::fromUtf8(GFGpgRecipientPubAlgo(list, i)), - QString::fromUtf8(GFGpgRecipientFingerprint(list, i)), - QString::fromUtf8(GFGpgRecipientUid(list, i)), - GFGpgRecipientKeyFound(list, i) != 0, - GFGpgRecipientHasSecret(list, i) != 0, - GFGpgRecipientHidden(list, i) != 0, + QString::fromUtf8( + GFGpgRecipientAt(GFModuleSdkContext(), list, i)->key_id), + QString::fromUtf8( + GFGpgRecipientAt(GFModuleSdkContext(), list, i)->pub_algo), + QString::fromUtf8( + GFGpgRecipientAt(GFModuleSdkContext(), list, i)->fingerprint), + QString::fromUtf8( + GFGpgRecipientAt(GFModuleSdkContext(), list, i)->uid), + GFGpgRecipientAt(GFModuleSdkContext(), list, i)->key_found != 0, + GFGpgRecipientAt(GFModuleSdkContext(), list, i)->has_secret != 0, + GFGpgRecipientAt(GFModuleSdkContext(), list, i)->hidden != 0, }); } - GFGpgRecipientListRelease(list); + GFGpgRecipientRelease(GFModuleSdkContext(), list); if (!recipients.isEmpty()) { show_decrypt_capability(DescribeDecryptCapability(recipients)); @@ -1991,7 +1996,7 @@ void EMailPageView::show_decrypt_capability( case EMailDecryptVerdict::kCANNOT_OPEN: EMailSetLabelColor(locked_capability_, - ThemeColor(this, &GFUIWarningColor)); + ThemeColor(this, GF_UI_COLOR_WARNING)); locked_capability_->setText( tr("This message is encrypted to keys you do not hold the private " "half of, so it cannot be opened on this computer.")); @@ -2018,19 +2023,21 @@ void EMailPageView::refresh_locked_capability_by_address( // so it is reached only when the message itself could not be read, and every // wording below is hedged accordingly. GFStringListRef addresses = nullptr; - if (GFGpgListAddresses(GFGpgCurrentGpgContextChannel(), 1, &addresses) != 0) { + if (GFGpgListAddresses(GFModuleSdkContext(), + GFGpgCurrentChannel(GFModuleSdkContext()), 1, + &addresses) != 0) { locked_capability_->setVisible(false); return; } QStringList mine; - const auto count = GFStringListCount(addresses); + const auto count = GFStringListCount(GFModuleSdkContext(), addresses); for (size_t i = 0; i < count; ++i) { - const auto address = - AddressOfUid(QString::fromUtf8(GFStringListAt(addresses, i))); + const auto address = AddressOfUid( + QString::fromUtf8(GFStringListAt(GFModuleSdkContext(), addresses, i))); if (!address.isEmpty()) mine.append(address.toLower()); } - GFStringListRelease(addresses); + GFStringListRelease(GFModuleSdkContext(), addresses); // Nothing knowable either way: no recipient is named, or the keyring holds // no private key at all. Saying something in that case would be inventing @@ -2054,7 +2061,8 @@ void EMailPageView::refresh_locked_capability_by_address( locked_capability_->setVisible(true); if (matched.isEmpty()) { - EMailSetLabelColor(locked_capability_, ThemeColor(this, &GFUIWarningColor)); + EMailSetLabelColor(locked_capability_, + ThemeColor(this, GF_UI_COLOR_WARNING)); locked_capability_->setText( tr("You do not hold a private key for any of these addresses. Unless " "the message was also encrypted to a key that is not named here, " @@ -2254,8 +2262,8 @@ void EMailPageView::refresh_security() { security_view_->SetMessage( security_state_, regions_, cached_verification_.signatures, recipient_rows_, addresses, message_.from, - GFGpgCurrentGpgContextChannel(), findings, cached_verification_.state, - !message_key_parts().isEmpty()); + GFGpgCurrentChannel(GFModuleSdkContext()), findings, + cached_verification_.state, !message_key_parts().isEmpty()); } /** @@ -2362,20 +2370,22 @@ void EMailPageView::refresh_send_state() { void EMailPageView::install_address_hints() { const auto addresses = [](bool secret_only) { GFStringListRef raw = nullptr; - if (GFGpgListAddresses(GFGpgCurrentGpgContextChannel(), secret_only ? 1 : 0, - &raw) != 0) { + if (GFGpgListAddresses(GFModuleSdkContext(), + GFGpgCurrentChannel(GFModuleSdkContext()), + secret_only ? 1 : 0, &raw) != 0) { return QStringList{}; } - const auto count = GFStringListCount(raw); + const auto count = GFStringListCount(GFModuleSdkContext(), raw); QStringList out; out.reserve(static_cast<qsizetype>(count)); // Copied rather than taken: the accessor returns a BORROWED pointer that // dies with the list, which goes back in a single release below. for (size_t i = 0; i < count; ++i) { - out.append(QString::fromUtf8(GFStringListAt(raw, i))); + out.append( + QString::fromUtf8(GFStringListAt(GFModuleSdkContext(), raw, i))); } - GFStringListRelease(raw); + GFStringListRelease(GFModuleSdkContext(), raw); return out; }; @@ -3133,7 +3143,7 @@ void EMailPageView::slot_selection_changed() { } void EMailPageView::slot_add_attachment() { - auto default_dir = UnStrDup(GFUIDefaultUserFilePath()); + auto default_dir = gf::sdk::UserFilePath(GFModuleSdkContext()); if (default_dir.isEmpty()) default_dir = QDir::homePath(); attach_paths( @@ -3324,9 +3334,9 @@ void EMailPageView::import_message_keys() { // them carries them for the same reason, and asking twice about one act the // user already asked for would be a dialog per part. for (const auto* part : keys) { - GFGpgImportKeys(GFGpgCurrentGpgContextChannel(), this, - part->data.constData(), - static_cast<int>(part->data.size())); + gf::sdk::ImportKeys(GFModuleSdkContext(), + GFGpgCurrentChannel(GFModuleSdkContext()), this, + part->data); } // The keyring is what the Security tab was reporting against, so it has to @@ -3340,8 +3350,9 @@ void EMailPageView::import_attachment_key(const EMailAttachment& att) { // The host owns the import, including whatever it wants to say about what // came of it: this module has no business inventing a second report of the // same operation. - GFGpgImportKeys(GFGpgCurrentGpgContextChannel(), this, att.data.constData(), - static_cast<int>(att.data.size())); + gf::sdk::ImportKeys(GFModuleSdkContext(), + GFGpgCurrentChannel(GFModuleSdkContext()), this, + att.data); } void EMailPageView::open_attachment(QTreeWidgetItem* item) { @@ -3506,7 +3517,7 @@ void EMailPageView::slot_save_all_attachments() { void EMailPageView::save_attachments(const QList<EMailAttachment>& chosen) { if (chosen.isEmpty()) return; - auto default_dir = UnStrDup(GFUIDefaultUserFilePath()); + auto default_dir = gf::sdk::UserFilePath(GFModuleSdkContext()); if (default_dir.isEmpty()) default_dir = QDir::homePath(); if (chosen.size() == 1) { diff --git a/src/m_email/EMailSecret.cpp b/src/m_email/EMailSecret.cpp index a8f6ea3..7068edd 100644 --- a/src/m_email/EMailSecret.cpp +++ b/src/m_email/EMailSecret.cpp @@ -56,17 +56,20 @@ void EMailSecret::Wipe() { bytes_.shrink_to_fit(); } -auto EMailSecret::AdoptCString(char* source) -> std::shared_ptr<EMailSecret> { +auto EMailSecret::AdoptBuffer(GFSDKContext* ctx, GFBufferRef source) + -> std::shared_ptr<EMailSecret> { auto secret = std::make_shared<EMailSecret>(); if (source == nullptr) return secret; - const auto length = std::strlen(source); - secret->bytes_.assign(source, source + length); + const auto* data = static_cast<const char*>(GFBufferData(ctx, source)); + const auto size = GFBufferSize(ctx, source); + if (data != nullptr && size != 0) { + secret->bytes_.assign(data, data + size); + } - // The SDK's buffer is wiped and returned to the secure allocator it came - // from, so the secret exists in exactly one place from here on. - SecureZero(source, length); - GFSecFreeMemory(static_cast<void*>(source)); + // Release wipes: the secret exists in exactly one place from here on, and + // that place is a vector this class can erase. + GFBufferRelease(ctx, source); return secret; } @@ -85,8 +88,9 @@ auto EMailSecret::CopyFrom(const QString& text) } auto EMailSecret::ToSecureCString() const -> char* { - auto* buffer = static_cast<char*>( - GFSecAllocateMemory(static_cast<uint32_t>(bytes_.size() + 1))); + auto* buffer = + static_cast<char*>(GFMemAlloc(GFModuleSdkContext(), GF_ARENA_SECURE, + static_cast<uint32_t>(bytes_.size() + 1))); if (buffer == nullptr) return nullptr; if (!bytes_.empty()) std::memcpy(buffer, bytes_.data(), bytes_.size()); diff --git a/src/m_email/EMailSecret.h b/src/m_email/EMailSecret.h index ad40da3..9ee3ed1 100644 --- a/src/m_email/EMailSecret.h +++ b/src/m_email/EMailSecret.h @@ -28,6 +28,8 @@ #pragma once +#include <GFSDKBuffer.h> + #include <QMetaType> #include <memory> #include <string> @@ -59,9 +61,13 @@ class EMailSecret { EMailSecret() = default; ~EMailSecret() { Wipe(); } - /// Takes the bytes of a NUL-terminated C string, then wipes the source. - /// For the SDK's secure buffers, which arrive as char*. - static auto AdoptCString(char* source) -> std::shared_ptr<EMailSecret>; + /// Takes the bytes out of an SDK buffer, then wipes and releases it. + /// + /// A buffer rather than a char*: the SDK hands secrets back as GFBufferRef + /// now, and a buffer wipes itself on release, so the secret never sits in + /// an ordinary allocation on the way here. + static auto AdoptBuffer(GFSDKContext* ctx, GFBufferRef source) + -> std::shared_ptr<EMailSecret>; /// Copies from a QString, for text the user typed into a field. The QString /// itself cannot be erased -- see the class note -- so the shorter its life @@ -105,6 +111,11 @@ class EMailSecret { /// allocated. Never free it yourself once it has been handed over. [[nodiscard]] auto ToSecureCString() const -> char*; + /// The raw bytes, borrowed, for handing straight into a wipeable SDK + /// buffer. Deliberately not a QString or QByteArray: both are implicitly + /// shared and neither can be erased afterwards. + [[nodiscard]] auto Data() const -> const char* { return bytes_.data(); } + /// Overwrites the buffer and releases it. Safe to call more than once. void Wipe(); diff --git a/src/m_email/EMailSecurityView.cpp b/src/m_email/EMailSecurityView.cpp index 2f32af5..8d3eb0f 100644 --- a/src/m_email/EMailSecurityView.cpp +++ b/src/m_email/EMailSecurityView.cpp @@ -533,8 +533,10 @@ void EMailSecurityView::add_key_section(const QStringList& addresses, // The address stays ours: every SDK argument is borrowed, so the // QByteArray below is safe to let die at the end of the statement. GFGpgKeyBriefListRef briefs = nullptr; - GFGpgFindKeys(channel, email.toUtf8().constData(), &briefs); - const auto count = static_cast<int>(GFGpgKeyBriefListCount(briefs)); + GFGpgFindKeys(GFModuleSdkContext(), channel, email.toUtf8().constData(), + &briefs); + const auto count = + static_cast<int>(GFGpgKeyBriefCount(GFModuleSdkContext(), briefs)); auto* item = new QTreeWidgetItem(group); item->setText(kColItem, email); @@ -545,7 +547,7 @@ void EMailSecurityView::add_key_section(const QStringList& addresses, // Recorded on the row so the menu knows this is an address with nothing // behind it, which is the one case where importing is worth offering. item->setData(kColItem, kRoleMissingKey, true); - GFGpgKeyBriefListRelease(briefs); + GFGpgKeyBriefRelease(GFModuleSdkContext(), briefs); continue; } @@ -556,12 +558,14 @@ void EMailSecurityView::add_key_section(const QStringList& addresses, // Borrowed accessors throughout: nothing here is separately releasable, // so the whole list goes back in one call at the end. auto* key_item = new QTreeWidgetItem(item); - key_item->setText(kColItem, - QString::fromUtf8(GFGpgKeyBriefUid(briefs, i))); + key_item->setText( + kColItem, QString::fromUtf8( + GFGpgKeyBriefAt(GFModuleSdkContext(), briefs, i)->uid)); // Usability. Says whether the key can be used at all -- and nothing // whatsoever about whose key it is. - const auto usability = GFGpgKeyBriefUsability(briefs, i); + const auto usability = + GFGpgKeyBriefAt(GFModuleSdkContext(), briefs, i)->usability; key_item->setText(kColValue, tr("key is %1").arg(DescribeUsability(usability))); EMailSetCellTone( @@ -570,8 +574,10 @@ void EMailSecurityView::add_key_section(const QStringList& addresses, auto* fpr = new QTreeWidgetItem(key_item); fpr->setText(kColItem, tr("Fingerprint")); - fpr->setText(kColValue, - QString::fromUtf8(GFGpgKeyBriefFingerprint(briefs, i))); + fpr->setText( + kColValue, + QString::fromUtf8( + GFGpgKeyBriefAt(GFModuleSdkContext(), briefs, i)->fingerprint)); EMailSetCellTone(fpr, kColItem, EMailTone::kMUTED, this); // Identity binding, kept as its own row. A usable key carrying this @@ -579,11 +585,13 @@ void EMailSecurityView::add_key_section(const QStringList& addresses, // single combined verdict would bury. auto* identity = new QTreeWidgetItem(key_item); identity->setText(kColItem, tr("Identity")); - if (GFGpgKeyBriefMatchedUidRevoked(briefs, i) != 0) { + if (GFGpgKeyBriefAt(GFModuleSdkContext(), briefs, i) + ->matched_uid_revoked != 0) { identity->setText( kColValue, tr("this address is on a REVOKED user ID of the key")); EMailSetCellTone(identity, kColValue, EMailTone::kWARN, this); - } else if (GFGpgKeyBriefMatchedUidIsPrimary(briefs, i) != 0) { + } else if (GFGpgKeyBriefAt(GFModuleSdkContext(), briefs, i) + ->matched_uid_is_primary != 0) { identity->setText(kColValue, tr("this address is the key's primary " "user ID")); EMailSetCellTone(identity, kColValue, EMailTone::kGOOD, this); @@ -593,7 +601,7 @@ void EMailSecurityView::add_key_section(const QStringList& addresses, EMailSetCellTone(identity, kColValue, EMailTone::kMUTED, this); } - if (GFGpgKeyBriefCanEncrypt(briefs, i) == 0) { + if (GFGpgKeyBriefAt(GFModuleSdkContext(), briefs, i)->can_encrypt == 0) { auto* note = new QTreeWidgetItem(key_item); note->setText(kColItem, tr("Note")); note->setText(kColValue, tr("this key cannot be used for encryption")); @@ -601,7 +609,7 @@ void EMailSecurityView::add_key_section(const QStringList& addresses, } } - GFGpgKeyBriefListRelease(briefs); + GFGpgKeyBriefRelease(GFModuleSdkContext(), briefs); } } diff --git a/src/m_email/EMailStructureView.cpp b/src/m_email/EMailStructureView.cpp index b04b1d7..d470556 100644 --- a/src/m_email/EMailStructureView.cpp +++ b/src/m_email/EMailStructureView.cpp @@ -190,9 +190,9 @@ void EMailStructureView::show_part_menu(const QPoint& pos) { save_part(*part); } else if (import != nullptr && chosen == import) { // The host owns the import and whatever it reports about the outcome. - GFGpgImportKeys(GFGpgCurrentGpgContextChannel(), this, - part->data.constData(), - static_cast<int>(part->data.size())); + gf::sdk::ImportKeys(GFModuleSdkContext(), + GFGpgCurrentChannel(GFModuleSdkContext()), this, + part->data); } else if (chosen == copy_type) { clipboard->setText(part->content_type); } else if (chosen == copy_name) { diff --git a/src/m_email/EMailViewStyle.h b/src/m_email/EMailViewStyle.h index e52dc14..3968b98 100644 --- a/src/m_email/EMailViewStyle.h +++ b/src/m_email/EMailViewStyle.h @@ -41,6 +41,7 @@ #include <QTreeWidgetItem> #include <QVBoxLayout> #include <QWidget> +#include <array> #include <functional> #include "GFModule.h" @@ -58,34 +59,39 @@ * de-emphasised rather than red. Red belongs to irreversible acts and to * secrets travelling in the clear, not to "this part is unsigned". */ -inline auto EMailThemeColor(QWidget* w, uint32_t (*getter)(void*)) -> QColor { - const auto rgba = getter(w); +/// One colour, by role. Five host entry points became one taking a role, so +/// the function pointer this used to take is now an int. +inline auto EMailThemeColor(QWidget* w, int role) -> QColor { + const auto rgba = GFUIThemeColor(GFModuleSdkContext(), role, w); return rgba == 0 ? w->palette().color(QPalette::WindowText) : QColor::fromRgba(rgba); } inline auto EMailMutedColor(QWidget* w) -> QColor { - return EMailThemeColor(w, &GFUIMutedTextColor); + return EMailThemeColor(w, GF_UI_COLOR_MUTED_TEXT); } inline auto EMailWarningColor(QWidget* w) -> QColor { - return EMailThemeColor(w, &GFUIWarningColor); + return EMailThemeColor(w, GF_UI_COLOR_WARNING); } inline auto EMailAccentColor(QWidget* w, bool positive) -> QColor { - const auto rgba = GFUIAccentColor(w, positive ? 1 : 0); - return rgba == 0 ? w->palette().color(QPalette::WindowText) - : QColor::fromRgba(rgba); + return EMailThemeColor( + w, positive ? GF_UI_COLOR_ACCENT_POSITIVE : GF_UI_COLOR_ACCENT_NEGATIVE); } /// A size written the way the rest of the application writes it. +/// +/// Pure: it formats into a local buffer and needs no context at all. inline auto EMailHumanSize(qint64 bytes) -> QString { - return UnStrDup(GFUIHumanSize(bytes)); + std::array<char, 64> text{}; + const auto written = GFUIHumanSize(bytes, text.data(), text.size()); + return written < 0 ? QString() : QString::fromUtf8(text.data(), written); } /// The application's own border colour, for the one-pixel rules around cards. inline auto EMailBorderColor(QWidget* w) -> QColor { - return EMailThemeColor(w, &GFUIBorderColor); + return EMailThemeColor(w, GF_UI_COLOR_BORDER); } /// @p a mixed with @p b, @p t of the way towards @p b. @@ -333,7 +339,8 @@ inline auto EMailTintedBanner(QWidget* parent, const QColor& tint, * nothing here is worth refusing to build a widget over. */ inline auto EMailViewSettings() -> QSettings* { - return qobject_cast<QSettings*>(static_cast<QObject*>(GFUIGlobalSettings())); + return qobject_cast<QSettings*>( + static_cast<QObject*>(GFStorageSettingsRoot(GFModuleSdkContext()))); } /** @@ -370,7 +377,7 @@ inline auto EMailToneColor(QWidget* owner, EMailTone tone) -> QColor { case EMailTone::kWARN: return EMailWarningColor(owner); case EMailTone::kDANGER: - return EMailThemeColor(owner, &GFUIDangerColor); + return EMailThemeColor(owner, GF_UI_COLOR_DANGER); case EMailTone::kDEFAULT: break; } diff --git a/src/m_email/module.json b/src/m_email/module.json index 92f0104..f27d188 100644 --- a/src/m_email/module.json +++ b/src/m_email/module.json @@ -7,8 +7,9 @@ "author": "Saturneric", "capabilities": [ "gpg", - "ui", - "network" + "network", + "storage", + "ui" ], "events": [ "EDIT_TAB_TYPE_EMAIL_OP_DECRYPT", diff --git a/src/m_email/test/CMakeLists.txt b/src/m_email/test/CMakeLists.txt index 3186cb0..d38182b 100644 --- a/src/m_email/test/CMakeLists.txt +++ b/src/m_email/test/CMakeLists.txt @@ -116,14 +116,17 @@ target_include_directories(gf_mod_email_net_test PRIVATE "${CMAKE_BINARY_DIR}/src" "${CMAKE_BINARY_DIR}/src/sdk") +# The REAL module-side SDK, against a fake host table the harness supplies. +# That is the point of the split: the SDK is stateless and takes a context, so +# a test can hand it one without a host anywhere in the process. target_link_libraries(gf_mod_email_net_test PRIVATE GTest::gtest GTest::gtest_main Qt::Core Qt::Network vmime-static - OpenSSL::SSL OpenSSL::Crypto) + OpenSSL::SSL OpenSSL::Crypto gf_sdk) -# The stubs stand in for the SDK, so the real export macros must expand to -# nothing rather than to a visibility attribute on a symbol we define here. +# Only the module export macro needs neutering now: nothing here defines an +# SDK symbol any more. target_compile_definitions(gf_mod_email_net_test PRIVATE - VMIME_STATIC "GF_SDK_EXPORT=" "GF_MODULE_EXPORT=") + VMIME_STATIC "GF_MODULE_EXPORT=") set_target_properties(gf_mod_email_net_test PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${GPGFRONTEND_MODULE_TEST_OUTPUT_DIR}") @@ -164,10 +167,10 @@ target_include_directories(gf_mod_email_crypto_test PRIVATE "${CMAKE_BINARY_DIR}/src/sdk") target_link_libraries(gf_mod_email_crypto_test PRIVATE - GTest::gtest GTest::gtest_main Qt::Core Qt::Network vmime-static) + GTest::gtest GTest::gtest_main Qt::Core Qt::Network vmime-static gf_sdk) target_compile_definitions(gf_mod_email_crypto_test PRIVATE - VMIME_STATIC "GF_SDK_EXPORT=" "GF_MODULE_EXPORT=") + VMIME_STATIC "GF_MODULE_EXPORT=") set_target_properties(gf_mod_email_crypto_test PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${GPGFRONTEND_MODULE_TEST_OUTPUT_DIR}") diff --git a/src/m_email/test/EMailCryptoSdkStubs.cpp b/src/m_email/test/EMailCryptoSdkStubs.cpp index 0e322ef..2553b7f 100644 --- a/src/m_email/test/EMailCryptoSdkStubs.cpp +++ b/src/m_email/test/EMailCryptoSdkStubs.cpp @@ -41,7 +41,10 @@ #include "EMailCryptoRecorder.h" // +#include <GFSDKBuildInfo.h> +#include <GFSDKContext.h> #include <GFSDKGpg.h> +#include <GFSDKHostApi.h> #include <QByteArray> #include <cstdint> @@ -79,15 +82,15 @@ auto Get() -> Recording& { return g_recording; } using crypto_recorder::g_recording; -extern "C" { +namespace { -void* GFAllocateMemory(unsigned int n) { +void* MemAllocNormal(unsigned int n) { void* p = std::malloc(n); NormalArena().insert(p); return p; } -void GFFreeMemory(void* p) { +void MemFreeNormal(void* p) { if (p == nullptr) return; if (NormalArena().erase(p) == 0) { std::printf("[FAIL] GFFreeMemory on a pointer it does not own\n"); @@ -97,13 +100,13 @@ void GFFreeMemory(void* p) { std::free(p); } -void* GFSecAllocateMemory(unsigned int n) { +void* MemAllocSecure(unsigned int n) { void* p = std::malloc(n); SecureArena().insert(p); return p; } -void GFSecFreeMemory(void* p) { +void MemFreeSecure(void* p) { if (p == nullptr) return; if (SecureArena().erase(p) == 0) { std::printf("[FAIL] GFSecFreeMemory on a pointer it does not own\n"); @@ -114,42 +117,29 @@ void GFSecFreeMemory(void* p) { } // Deliberately identical to the shipping implementation, truncation and all. -char* GFModuleStrDup(const char* s) { +char* StrDupNormal(const char* s) { if (s == nullptr) return nullptr; auto n = std::strlen(s) + 1; - char* d = static_cast<char*>(GFAllocateMemory(n)); + char* d = static_cast<char*>(MemAllocNormal(n)); std::memcpy(d, s, n); return d; } -char* GFModuleSecStrDup(const char* s) { +char* StrDupSecure(const char* s) { if (s == nullptr) return nullptr; auto n = std::strlen(s) + 1; - char* d = static_cast<char*>(GFSecAllocateMemory(n)); + char* d = static_cast<char*>(MemAllocSecure(n)); std::memcpy(d, s, n); return d; } -void GFModuleLogTrace(const char*) {} -void GFModuleLogDebug(const char*) {} -void GFModuleLogInfo(const char*) {} -void GFModuleLogWarn(const char*) {} -void GFModuleLogError(const char*) {} -void GFModuleLogAt(const char*, int, const char*, int, const char*, - const char*) {} -int GFModuleLogEnabled(const char*, int) { return 1; } - -char* GFAppActiveLocale() { return GFModuleStrDup("en_US"); } +void LogWrite(GFHostContextRef, int, const char*, int, const char*, + const char*) {} +auto LogEnabled(GFHostContextRef, int) -> int { return 1; } -char* GFGpgPublicKey(int, const char* key_id, int) { - // Borrowed, not consumed: the real entry point stopped freeing its - // arguments, and a stub that still frees would report a bogus allocator - // violation for every correct call. - (void)key_id; - return GFModuleStrDup( - "-----BEGIN PGP PUBLIC KEY BLOCK-----\nstub\n" - "-----END PGP PUBLIC KEY BLOCK-----\n"); -} +auto AppText(GFHostContextRef) -> const char* { return "test"; } +auto AppLocale(GFHostContextRef) -> char* { return StrDupNormal("en_US"); } +auto AppZero(GFHostContextRef) -> int { return 0; } // --- the four crypto entry points ------------------------------------------- // @@ -161,7 +151,7 @@ namespace { // Mirrors GFBytesDup on the host side: byte-exact, NUL-terminated one past the // end, with the true length reported separately. auto BytesOut(const QByteArray& b, size_t* size) -> char* { - auto* d = static_cast<char*>(GFAllocateMemory(b.size() + 1)); + auto* d = static_cast<char*>(MemAllocNormal(b.size() + 1)); std::memcpy(d, b.constData(), b.size()); d[b.size()] = '\0'; if (size != nullptr) *size = static_cast<size_t>(b.size()); @@ -169,12 +159,18 @@ auto BytesOut(const QByteArray& b, size_t* size) -> char* { } } // namespace -int GFAnalyseVerifyResultInfoByCapsule(int, uint32_t, const char* capsule_id, - const char** analyse, const char** cards, - const char** info_json) { +auto GpgAnalyse(GFHostContextRef, int, int, uint32_t, const char* capsule_id, + uint32_t, GFGpgAnalysis* out) -> int { (void)capsule_id; // borrowed, like every SDK argument - if (analyse != nullptr) *analyse = GFModuleStrDup(""); - if (cards != nullptr) *cards = GFModuleStrDup("[]"); + if (out == nullptr) return -1; + out->struct_size = sizeof(GFGpgAnalysis); + + // Nine entry points became one filling a struct, so the fake fills it. + const char** analyse = const_cast<const char**>(&out->report); + const char** cards = const_cast<const char**>(&out->cards); + const char** info_json = const_cast<const char**>(&out->info_json); + if (analyse != nullptr) *analyse = StrDupNormal(""); + if (cards != nullptr) *cards = StrDupNormal("[]"); // One good signature, so ParseSignatureResults has something to walk -- // unless the test queued an answer for this call. Regions are analysed in @@ -183,9 +179,9 @@ int GFAnalyseVerifyResultInfoByCapsule(int, uint32_t, const char* capsule_id, auto& recording = crypto_recorder::Get(); if (!recording.verify_info_json.isEmpty()) { const auto queued = recording.verify_info_json.takeFirst(); - *info_json = GFModuleStrDup(queued.constData()); + *info_json = StrDupNormal(queued.constData()); } else { - *info_json = GFModuleStrDup( + *info_json = StrDupNormal( R"({"signatures":[{"status":"good","fingerprint":"DEADBEEF",)" R"("hash_algo":"SHA256","uid":"Test <[email protected]>"}]})"); } @@ -193,7 +189,7 @@ int GFAnalyseVerifyResultInfoByCapsule(int, uint32_t, const char* capsule_id, return 0; } -} // extern "C" +} // namespace /* ===================================================================== * * Handle-based SDK: buffers and results @@ -282,7 +278,7 @@ auto KeyIdCount(const char* const* key_ids, size_t n) -> int { extern "C" { -GFBufferRef GFBufferNewFromBytes(const void* data, size_t size) { +auto BufNew(GFHostContextRef, const void* data, size_t size) -> GFBufferRef { if (data == nullptr && size != 0) return nullptr; auto* b = new StubBuffer{ QByteArray(static_cast<const char*>(data), static_cast<int>(size))}; @@ -290,22 +286,34 @@ GFBufferRef GFBufferNewFromBytes(const void* data, size_t size) { return reinterpret_cast<GFBufferRef>(b); } -const void* GFBufferData(GFBufferView buf) { +auto GpgPublicKey(GFHostContextRef ctx, int, const char* key_id, int) + -> GFBufferRef { + // Borrowed, not consumed: the real entry point stopped freeing its + // arguments, and a stub that still frees would report a bogus allocator + // violation for every correct call. + (void)key_id; + static constexpr char kBlock[] = + "-----BEGIN PGP PUBLIC KEY BLOCK-----\nstub\n" + "-----END PGP PUBLIC KEY BLOCK-----\n"; + return BufNew(ctx, kBlock, sizeof(kBlock) - 1); +} + +auto BufData(GFHostContextRef, GFBufferView buf) -> const void* { auto* impl = LiveBuf(buf); return impl == nullptr ? nullptr : impl->bytes.constData(); } -size_t GFBufferSize(GFBufferView buf) { +auto BufSize(GFHostContextRef, GFBufferView buf) -> size_t { auto* impl = LiveBuf(buf); return impl == nullptr ? 0 : static_cast<size_t>(impl->bytes.size()); } -void GFBufferZeroize(GFBufferRef buf) { +void BufZeroize(GFHostContextRef, GFBufferRef buf) { auto* impl = LiveBuf(buf); if (impl != nullptr) impl->bytes.fill('\0'); } -void GFBufferRelease(GFBufferRef buf) { +void BufRelease(GFHostContextRef, GFBufferRef buf) { if (buf == nullptr) return; if (LiveBuffers().erase(buf) == 0) { std::printf("[FAIL] GFBufferRelease on a handle it does not own\n"); @@ -315,10 +323,11 @@ void GFBufferRelease(GFBufferRef buf) { delete reinterpret_cast<StubBuffer*>(buf); } -size_t GFBufferOutstandingCount(const char*) { return LiveBuffers().size(); } +auto BufOutstanding(GFHostContextRef) -> size_t { return LiveBuffers().size(); } -int GFGpgSign(int, const char* const* key_ids, size_t key_ids_size, - GFBufferView in, int sign_mode, int, GFGpgResultRef* out) { +auto GpgSign(GFHostContextRef, int, const char* const* key_ids, + size_t key_ids_size, GFBufferView in, int sign_mode, int, + GFGpgResultRef* out) -> int { if (out == nullptr) return -1; g_recording.sign.append({BufferBytes(in), sign_mode}); (void)KeyIdCount(key_ids, key_ids_size); @@ -331,8 +340,8 @@ int GFGpgSign(int, const char* const* key_ids, size_t key_ids_size, return GF_GPG_OP_FAILED; } - r->data = GFBufferNewFromBytes(g_recording.sign_output.constData(), - g_recording.sign_output.size()); + r->data = BufNew(nullptr, g_recording.sign_output.constData(), + g_recording.sign_output.size()); r->hash_algo = "SHA256"; r->capsule_id = "stub-capsule"; r->error_string = "Success"; @@ -340,8 +349,9 @@ int GFGpgSign(int, const char* const* key_ids, size_t key_ids_size, return GF_GPG_OK; } -int GFGpgEncrypt(int, const char* const* key_ids, size_t key_ids_size, - GFBufferView in, int, GFGpgResultRef* out) { +auto GpgEncrypt(GFHostContextRef, int, const char* const* key_ids, + size_t key_ids_size, GFBufferView in, int, GFGpgResultRef* out) + -> int { if (out == nullptr) return -1; g_recording.encrypt.append({BufferBytes(in)}); (void)KeyIdCount(key_ids, key_ids_size); @@ -354,15 +364,16 @@ int GFGpgEncrypt(int, const char* const* key_ids, size_t key_ids_size, return GF_GPG_OP_FAILED; } - r->data = GFBufferNewFromBytes(g_recording.encrypt_output.constData(), - g_recording.encrypt_output.size()); + r->data = BufNew(nullptr, g_recording.encrypt_output.constData(), + g_recording.encrypt_output.size()); r->capsule_id = "stub-capsule"; r->error_string = "Success"; *out = reinterpret_cast<GFGpgResultRef>(r); return GF_GPG_OK; } -int GFGpgDecrypt(int, GFBufferView in, GFGpgResultRef* out) { +auto GpgDecrypt(GFHostContextRef, int, GFBufferView in, GFGpgResultRef* out) + -> int { if (out == nullptr) return -1; g_recording.decrypt.append({BufferBytes(in)}); @@ -374,16 +385,16 @@ int GFGpgDecrypt(int, GFBufferView in, GFGpgResultRef* out) { return GF_GPG_OP_FAILED; } - r->data = GFBufferNewFromBytes(g_recording.decrypt_output.constData(), - g_recording.decrypt_output.size()); + r->data = BufNew(nullptr, g_recording.decrypt_output.constData(), + g_recording.decrypt_output.size()); r->capsule_id = "stub-capsule"; r->error_string = "Success"; *out = reinterpret_cast<GFGpgResultRef>(r); return GF_GPG_OK; } -int GFGpgVerify(int, GFBufferView in, GFBufferView signature, - GFGpgResultRef* out) { +auto GpgVerify(GFHostContextRef, int, GFBufferView in, GFBufferView signature, + GFGpgResultRef* out) -> int { if (out == nullptr) return -1; g_recording.verify.append({BufferBytes(in), BufferBytes(signature)}); @@ -401,37 +412,37 @@ int GFGpgVerify(int, GFBufferView in, GFBufferView signature, return GF_GPG_OK; } -int GFGpgResultStatusOf(GFGpgResultRef r) { +auto GpgResultStatus(GFHostContextRef, GFGpgResultRef r) -> int { auto* impl = LiveRes(r); return impl == nullptr ? GF_GPG_BAD_REQUEST : impl->status; } -uint32_t GFGpgResultError(GFGpgResultRef r) { +auto GpgResultError(GFHostContextRef, GFGpgResultRef r) -> uint32_t { auto* impl = LiveRes(r); return impl == nullptr ? 0 : impl->error; } -GFBufferView GFGpgResultData(GFGpgResultRef r) { +auto GpgResultData(GFHostContextRef, GFGpgResultRef r) -> GFBufferView { auto* impl = LiveRes(r); return impl == nullptr ? nullptr : impl->data; } -const char* GFGpgResultCapsuleId(GFGpgResultRef r) { +auto ResultCapsuleId(GFGpgResultRef r) -> const char* { auto* impl = LiveRes(r); return impl == nullptr ? "" : impl->capsule_id.constData(); } -const char* GFGpgResultErrorString(GFGpgResultRef r) { +auto ResultErrorString(GFGpgResultRef r) -> const char* { auto* impl = LiveRes(r); return impl == nullptr ? "" : impl->error_string.constData(); } -const char* GFGpgResultHashAlgo(GFGpgResultRef r) { +auto ResultHashAlgo(GFGpgResultRef r) -> const char* { auto* impl = LiveRes(r); return impl == nullptr ? "" : impl->hash_algo.constData(); } -GFBufferRef GFGpgResultTakeData(GFGpgResultRef r) { +auto GpgResultTakeData(GFHostContextRef, GFGpgResultRef r) -> GFBufferRef { auto* impl = LiveRes(r); if (impl == nullptr) return nullptr; auto* taken = impl->data; @@ -439,7 +450,7 @@ GFBufferRef GFGpgResultTakeData(GFGpgResultRef r) { return taken; } -void GFGpgResultRelease(GFGpgResultRef r) { +void GpgResultRelease(GFHostContextRef, GFGpgResultRef r) { if (r == nullptr) return; if (LiveResults().erase(r) == 0) { std::printf("[FAIL] GFGpgResultRelease on a handle it does not own\n"); @@ -447,15 +458,134 @@ void GFGpgResultRelease(GFGpgResultRef r) { return; } auto* impl = reinterpret_cast<StubResult*>(r); - GFBufferRelease(impl->data); + BufRelease(nullptr, impl->data); delete impl; } -size_t GFGpgResultOutstandingCount(const char*) { return LiveResults().size(); } +auto GpgResultOutstanding(GFHostContextRef) -> size_t { + return LiveResults().size(); +} + +/* --- the tables ----------------------------------------------------------- */ -} // extern "C" +auto MemAlloc(GFHostContextRef, int arena, uint32_t n) -> void* { + return arena == GF_ARENA_SECURE ? MemAllocSecure(n) : MemAllocNormal(n); +} +auto MemRealloc(GFHostContextRef, int, void* p, uint32_t n) -> void* { + return std::realloc(p, n); +} +void MemFree(GFHostContextRef, int arena, void* p) { + if (arena == GF_ARENA_SECURE) { + MemFreeSecure(p); + } else { + MemFreeNormal(p); + } +} +auto MemStrDup(GFHostContextRef, int arena, const char* s) -> char* { + return arena == GF_ARENA_SECURE ? StrDupSecure(s) : StrDupNormal(s); +} + +/// Three named getters became one taking a field, so the fake follows. +auto GpgResultText(GFHostContextRef, GFGpgResultRef r, int field) -> const + char* { + switch (field) { + case GF_GPG_RESULT_TEXT_ERROR_STRING: + return ResultErrorString(r); + case GF_GPG_RESULT_TEXT_HASH_ALGO: + return ResultHashAlgo(r); + default: + return ResultCapsuleId(r); + } +} + +const GFHostBufferApi kBuffer = { + sizeof(GFHostBufferApi), + &BufNew, + &BufData, + &BufSize, + &BufZeroize, + &BufRelease, + &BufOutstanding, + &MemAlloc, + &MemRealloc, + &MemFree, + &MemStrDup, +}; + +const GFHostLogApi kLog = {sizeof(GFHostLogApi), &LogWrite, &LogEnabled}; + +const GFHostAppApi kApp = { + sizeof(GFHostAppApi), &AppText, &AppText, &AppText, &AppText, + &AppLocale, &AppZero, &AppZero, +}; + +auto NotImplementedInt(GFHostContextRef, ...) -> int { return -1; } + +const GFHostGpgApi kGpg = { + sizeof(GFHostGpgApi), + nullptr, // current_channel: these tests always name the channel + &GpgSign, + &GpgEncrypt, + &GpgDecrypt, + &GpgVerify, + &GpgResultStatus, + &GpgResultError, + &GpgResultData, + &GpgResultText, + &GpgResultTakeData, + &GpgResultRelease, + &GpgResultOutstanding, + &GpgAnalyse, + &GpgPublicKey, + nullptr, // key_primary_uid + nullptr, // export_key + nullptr, // import_keys + nullptr, // find_keys + nullptr, // key_brief_count + nullptr, // key_brief_at + nullptr, // key_brief_release + nullptr, // sniff_recipients + nullptr, // recipient_count + nullptr, // recipient_at + nullptr, // recipient_release + nullptr, // list_addresses +}; + +struct FakeHostContext { + int marker = 0; +}; + +auto TheTable() -> GFHostApi& { + static FakeHostContext host_context; + static GFHostApi host = [&]() { + GFHostApi t{}; + t.struct_size = sizeof(GFHostApi); + t.abi_version = GF_SDK_ABI_VERSION; + t.module_id = "harness"; + t.context = reinterpret_cast<GFHostContextRef>(&host_context); + t.buffer = &kBuffer; + t.log = &kLog; + t.app = &kApp; + t.gpg = &kGpg; + return t; + }(); + return host; +} + +} // namespace + +/// The module's SDK context, which the runtime would normally own. +auto GFModuleSdkContext() -> GFSDKContext* { + static GFSDKContext context = []() { + GFSDKContext c{}; + c.struct_size = sizeof(GFSDKContext); + c.abi_version = GF_SDK_ABI_VERSION; + c.host = &TheTable(); + c.module_id = "harness"; + return c; + }(); + return &context; +} -// C++ linkage, deliberately outside the extern "C" block above: GFModule.h -// declares it as an ordinary C++ function, and the log macros call it on every -// line. Defined here with C linkage it compiles cleanly and fails to link. +/// Identity, for the log macros. C++ linkage, as GFModule.h declares it. const char* GFGetModuleID() { return "harness"; } diff --git a/src/m_email/test/EMailSdkStubs.cpp b/src/m_email/test/EMailSdkStubs.cpp index 4ecee84..5b4e3ef 100644 --- a/src/m_email/test/EMailSdkStubs.cpp +++ b/src/m_email/test/EMailSdkStubs.cpp @@ -1,9 +1,21 @@ -// Stand-ins for the SDK symbols these translation units reference. +// A fake host, for the translation units that reach the SDK. +// +// What changed, and why it is a better harness: this file used to define the +// SDK's exported C symbols by hand. The SDK is now module-side and stateless, +// so the thing to fake is the HOST TABLE it is handed. Every GFSDK* call these +// tests make therefore runs the real wrapper code and lands here, which is +// what lets a test assert that the wrappers behave -- something a hand-written +// stub of the wrapper itself could never show. // // The allocator stubs are not merely placeholders: they enforce the same -// ownership contract the real SDK does, so that handing a function memory from -// the wrong arena is reported here instead of aborting the process with +// arena contract the real host does, so handing a function memory from the +// wrong arena is reported here instead of aborting the process with // munmap_chunk() at runtime. + +#include <GFSDKBuildInfo.h> +#include <GFSDKContext.h> +#include <GFSDKHostApi.h> + #include <QSettings> #include <QString> #include <cstdio> @@ -12,6 +24,7 @@ #include <set> namespace { + std::set<void*>& NormalArena() { static std::set<void*> a; return a; @@ -20,6 +33,7 @@ std::set<void*>& SecureArena() { static std::set<void*> a; return a; } + } // namespace int g_allocator_violations = 0; @@ -28,84 +42,155 @@ namespace harness { bool LastSaveUsedSecureValue = false; } -extern "C" { +namespace { + +/* --- buffers and memory --------------------------------------------------- */ -void* GFAllocateMemory(unsigned int n) { - void* p = std::malloc(n); - NormalArena().insert(p); - return p; +struct FakeBuffer { + size_t size; + char* data; +}; + +auto BufNew(GFHostContextRef, const void* data, size_t size) -> GFBufferRef { + auto* b = new FakeBuffer{size, static_cast<char*>(std::malloc(size + 1))}; + if (size != 0 && data != nullptr) std::memcpy(b->data, data, size); + b->data[size] = '\0'; + return reinterpret_cast<GFBufferRef>(b); } -void GFFreeMemory(void* p) { - if (p == nullptr) return; - if (NormalArena().erase(p) == 0) { - std::printf("[FAIL] GFFreeMemory on a pointer it does not own\n"); - ++g_allocator_violations; - return; - } - std::free(p); +auto BufData(GFHostContextRef, GFBufferView buf) -> const void* { + return buf == nullptr ? nullptr + : reinterpret_cast<const FakeBuffer*>(buf)->data; } -void* GFSecAllocateMemory(unsigned int n) { - void* p = std::malloc(n); - SecureArena().insert(p); +auto BufSize(GFHostContextRef, GFBufferView buf) -> size_t { + return buf == nullptr ? 0 : reinterpret_cast<const FakeBuffer*>(buf)->size; +} + +void BufZeroize(GFHostContextRef, GFBufferRef buf) { + if (buf == nullptr) return; + auto* b = reinterpret_cast<FakeBuffer*>(buf); + std::memset(b->data, 0, b->size); +} + +void BufRelease(GFHostContextRef, GFBufferRef buf) { + if (buf == nullptr) return; + auto* b = reinterpret_cast<FakeBuffer*>(buf); + // Release wipes, as the real one does: these tests carry passwords. + std::memset(b->data, 0, b->size); + std::free(b->data); + delete b; +} + +auto BufOutstanding(GFHostContextRef) -> size_t { return 0; } + +auto MemAlloc(GFHostContextRef, int arena, uint32_t n) -> void* { + auto* p = std::malloc(n); + if (p == nullptr) return nullptr; + (arena == GF_ARENA_SECURE ? SecureArena() : NormalArena()).insert(p); return p; } -void GFSecFreeMemory(void* p) { +auto MemRealloc(GFHostContextRef, int, void* p, uint32_t n) -> void* { + return std::realloc(p, n); +} + +void MemFree(GFHostContextRef, int arena, void* p) { if (p == nullptr) return; - if (SecureArena().erase(p) == 0) { - // Exactly the real crash: a pointer that never came from the secure - // allocator handed to the secure deallocator. + auto& mine = arena == GF_ARENA_SECURE ? SecureArena() : NormalArena(); + if (mine.erase(p) == 0) { std::printf( - "[FAIL] GFSecFreeMemory on a pointer it does not own " - "(this is the munmap_chunk abort)\n"); + "[FAIL] free from the wrong arena (this is the munmap_chunk abort)\n"); ++g_allocator_violations; return; } std::free(p); } -char* GFModuleStrDup(const char* s) { +auto MemStrDup(GFHostContextRef ctx, int arena, const char* s) -> char* { if (s == nullptr) return nullptr; - auto n = std::strlen(s) + 1; - char* d = static_cast<char*>(GFAllocateMemory(n)); - std::memcpy(d, s, n); - return d; + const auto n = std::strlen(s); + auto* copy = static_cast<char*>(MemAlloc(ctx, arena, n + 1)); + if (copy == nullptr) return nullptr; + std::memcpy(copy, s, n + 1); + return copy; } -char* GFModuleSecStrDup(const char* s) { - if (s == nullptr) return nullptr; - auto n = std::strlen(s) + 1; - char* d = static_cast<char*>(GFSecAllocateMemory(n)); - std::memcpy(d, s, n); - return d; +const GFHostBufferApi kBuffer = { + sizeof(GFHostBufferApi), + &BufNew, + &BufData, + &BufSize, + &BufZeroize, + &BufRelease, + &BufOutstanding, + &MemAlloc, + &MemRealloc, + &MemFree, + &MemStrDup, +}; + +/* --- log ------------------------------------------------------------------ */ + +void LogWrite(GFHostContextRef, int, const char*, int, const char*, + const char*) {} +auto LogEnabled(GFHostContextRef, int) -> int { return 1; } + +const GFHostLogApi kLog = {sizeof(GFHostLogApi), &LogWrite, &LogEnabled}; + +/* --- app ------------------------------------------------------------------ */ + +auto AppText(GFHostContextRef) -> const char* { return "test"; } +auto AppLocale(GFHostContextRef ctx) -> char* { + return MemStrDup(ctx, GF_ARENA_NORMAL, "en_US"); } +auto AppZero(GFHostContextRef) -> int { return 0; } +auto AppKeyProtection(GFHostContextRef) -> int { return 1; } -// Mirrors the real implementation's ownership behaviour: the key goes back to -// the ordinary allocator, the secret to the secure one. -int GFSecDurableCacheSave(const char* key, const char* value) { - if (key == nullptr || value == nullptr) return -1; - harness::LastSaveUsedSecureValue = - SecureArena().count(const_cast<char*>(value)) != 0; - GFFreeMemory(const_cast<char*>(key)); - GFSecFreeMemory(const_cast<char*>(value)); - return 0; +const GFHostAppApi kApp = { + sizeof(GFHostAppApi), &AppText, &AppText, &AppText, &AppText, + &AppLocale, &AppZero, &AppKeyProtection, +}; + +/* --- ui ------------------------------------------------------------------- */ + +auto ThemeColor(GFHostContextRef, int role, void*) -> uint32_t { + switch (role) { + case GF_UI_COLOR_WARNING: + return 0xFFFF8800; + case GF_UI_COLOR_DANGER: + return 0xFFFF0000; + case GF_UI_COLOR_ACCENT_POSITIVE: + case GF_UI_COLOR_ACCENT_NEGATIVE: + return 0xFF0066CC; + default: + return 0xFF808080; + } } -char* GFSecDurableCacheGet(const char* key) { - GFFreeMemory(const_cast<char*>(key)); +auto UiNull(GFHostContextRef, const char*) -> void* { return nullptr; } +auto UiCreate(GFHostContextRef, QObjectFactory, void*) -> void* { return nullptr; } - -int GFSecDurableCacheRemove(const char* key) { - GFFreeMemory(const_cast<char*>(key)); +auto UiShow(GFHostContextRef, void*, void*) -> int { return 0; } +auto UiPath(GFHostContextRef ctx) -> GFBufferRef { + return BufNew(ctx, "/tmp", 4); +} +auto UiRegSettings(GFHostContextRef, const GFUISettingsPageSpec*) -> int { return 0; } +auto UiUnreg(GFHostContextRef, const char*) -> int { return 0; } +auto UiRegTab(GFHostContextRef, const GFUITabViewSpec*) -> int { return 0; } +auto UiRegExt(GFHostContextRef, const char*, const char*) -> int { return 0; } -int GFAppKeyProtectionLevel() { return 1; } +const GFHostUiApi kUi = { + sizeof(GFHostUiApi), &UiCreate, &UiNull, &UiShow, &ThemeColor, &UiPath, + &UiRegSettings, &UiUnreg, &UiRegTab, &UiUnreg, &UiRegExt, +}; -void* GFUIGlobalSettings() { +/* --- storage -------------------------------------------------------------- */ + +auto SettingsRoot(GFHostContextRef) -> void* { static QSettings* settings = nullptr; if (settings == nullptr) { settings = new QSettings("/tmp/gf-focus-harness.ini", QSettings::IniFormat); @@ -113,25 +198,95 @@ void* GFUIGlobalSettings() { return settings; } -void GFModuleLogTrace(const char*) {} -void GFModuleLogDebug(const char*) {} -void GFModuleLogInfo(const char*) {} -void GFModuleLogWarn(const char*) {} -void GFModuleLogError(const char*) {} -void GFModuleLogAt(const char*, int, const char*, int, const char*, - const char*) {} -int GFModuleLogEnabled(const char*, int) { return 1; } +auto CacheGet(GFHostContextRef, int, const char*, GFBufferRef* out) -> int { + if (out != nullptr) *out = nullptr; + return -1; // nothing stored: absence is reportable now +} + +auto CacheSet(GFHostContextRef ctx, int store, const char*, GFBufferView value, + int64_t) -> int { + // The secret arrives as a buffer, which is the whole point: it never passes + // through an allocation the module cannot wipe. Recorded so the credential + // test can still assert the secure tier was the one used. + harness::LastSaveUsedSecureValue = + store == GF_STORE_SECURE_DURABLE && BufSize(ctx, value) != 0; + return 0; +} + +auto CacheRemove(GFHostContextRef, int, const char*) -> int { return 0; } +auto StateGetText(GFHostContextRef, const char*, const char*, GFBufferRef* out) + -> int { + if (out != nullptr) *out = nullptr; + return -1; +} +auto StateSetText(GFHostContextRef, const char*, const char*, GFBufferView) + -> int { + return 0; +} +auto StateGetBool(GFHostContextRef, const char*, const char*, int*) -> int { + return -1; +} +auto StateSetBool(GFHostContextRef, const char*, const char*, int) -> int { + return 0; +} +auto StateChildren(GFHostContextRef, const char*, const char*, + GFStringListRef* out) -> int { + if (out != nullptr) *out = nullptr; + return -1; +} + +const GFHostStorageApi kStorage = { + sizeof(GFHostStorageApi), + &SettingsRoot, + &CacheGet, + &CacheSet, + &CacheRemove, + &StateGetText, + &StateSetText, + &StateGetBool, + &StateSetBool, + &StateChildren, +}; + +/* --- the table ------------------------------------------------------------ */ + +struct FakeHostContext { + int marker = 0; +}; + +auto TheTable() -> GFHostApi& { + static FakeHostContext host_context; + static GFHostApi host = [&]() { + GFHostApi t{}; + t.struct_size = sizeof(GFHostApi); + t.abi_version = GF_SDK_ABI_VERSION; + t.module_id = "harness"; + t.context = reinterpret_cast<GFHostContextRef>(&host_context); + t.buffer = &kBuffer; + t.log = &kLog; + t.app = &kApp; + t.ui = &kUi; + t.storage = &kStorage; + return t; + }(); + return host; +} + +} // namespace -char* GFAppActiveLocale() { return GFModuleStrDup("en_US"); } -unsigned int GFUIMutedTextColor(void*) { return 0xFF808080; } -unsigned int GFUIBorderColor(void*) { return 0xFF808080; } -unsigned int GFUIWarningColor(void*) { return 0xFFFF8800; } -unsigned int GFUIDangerColor(void*) { return 0xFFFF0000; } -unsigned int GFUIAccentColor(void*, int) { return 0xFF0066CC; } -char* GFUIHumanSize(long long) { return GFModuleStrDup("0 B"); } +/// The module's SDK context, which the runtime would normally own. These +/// tests link no runtime, so the harness provides it. +auto GFModuleSdkContext() -> GFSDKContext* { + static GFSDKContext context = []() { + GFSDKContext c{}; + c.struct_size = sizeof(GFSDKContext); + c.abi_version = GF_SDK_ABI_VERSION; + c.host = &TheTable(); + c.module_id = "harness"; + return c; + }(); + return &context; } -// C++ linkage, deliberately outside the extern "C" block above: GFModule.h -// declares it as an ordinary C++ function, and the log macros call it on every -// line. Defined here with C linkage it compiles cleanly and fails to link. +/// Identity, for the log macros. C++ linkage, as GFModule.h declares it. const char* GFGetModuleID() { return "harness"; } diff --git a/src/m_email/test/EMailSecretTest.cpp b/src/m_email/test/EMailSecretTest.cpp index eb1455f..b36a99b 100644 --- a/src/m_email/test/EMailSecretTest.cpp +++ b/src/m_email/test/EMailSecretTest.cpp @@ -39,7 +39,8 @@ // this class exists is recorded rather than asserted. The rest hold // EMailSecret to the behaviour that replaces it. -#include <GFSDKBasic.h> +#include <GFSDKBuffer.h> +#include <GFSDKContext.h> #include <gtest/gtest.h> #include <QString> @@ -47,6 +48,9 @@ #include "EMailSecret.h" +/// Supplied by the harness (EMailSdkStubs.cpp). +auto GFModuleSdkContext() -> GFSDKContext*; + namespace { TEST(EMailSecretTest, AQStringWipeDoesNotReachASharedBuffer) { @@ -131,23 +135,25 @@ TEST(EMailSecretTest, TheLastHolderReleasingErasesTheBytes) { << "releasing one holder erased bytes the other still needed"; } -TEST(EMailSecretTest, AdoptingACStringWipesTheSource) { - // The SDK hands back a secure char*. Copying it out has to leave nothing - // behind in the buffer it came from. +TEST(EMailSecretTest, AdoptingABufferTakesTheBytesAndReleasesIt) { + // The SDK hands a secret back as a buffer now, not a secure char*. A buffer + // wipes itself on release, so the bytes exist in one place afterwards and + // that place is the vector this class can erase. const char* text = "from-the-sdk"; const auto length = std::strlen(text); - auto* raw = static_cast<char*>(GFSecAllocateMemory(length + 1)); - std::memcpy(raw, text, length + 1); + auto* ctx = GFModuleSdkContext(); + auto* raw = GFBufferNewFromBytes(ctx, text, length); + ASSERT_NE(raw, nullptr); - auto secret = EMailSecret::AdoptCString(raw); + auto secret = EMailSecret::AdoptBuffer(ctx, raw); EXPECT_EQ(secret->StdStringCopy(), "from-the-sdk"); EXPECT_EQ(secret->Size(), length); } TEST(EMailSecretTest, AdoptingNullIsEmptyRatherThanACrash) { - auto secret = EMailSecret::AdoptCString(nullptr); + auto secret = EMailSecret::AdoptBuffer(GFModuleSdkContext(), nullptr); ASSERT_NE(secret, nullptr); EXPECT_TRUE(secret->IsEmpty()); } @@ -180,7 +186,7 @@ TEST(EMailSecretTest, ASecureCStringCarriesTheSecretVerbatim) { // NUL-terminated one past the secret, so a C API can read it as a string. EXPECT_EQ(buffer[secret->Size()], '\0'); - GFSecFreeMemory(buffer); + GFMemFree(GFModuleSdkContext(), GF_ARENA_SECURE, buffer); } TEST(EMailSecretTest, AnEmptySecretStillProducesAUsableString) { @@ -190,7 +196,7 @@ TEST(EMailSecretTest, AnEmptySecretStillProducesAUsableString) { ASSERT_NE(buffer, nullptr); EXPECT_STREQ(buffer, ""); - GFSecFreeMemory(buffer); + GFMemFree(GFModuleSdkContext(), GF_ARENA_SECURE, buffer); } TEST(EMailSecretTest, NonAsciiSurvivesTheRoundTrip) { @@ -202,7 +208,7 @@ TEST(EMailSecretTest, NonAsciiSurvivesTheRoundTrip) { ASSERT_NE(buffer, nullptr); EXPECT_EQ(QString::fromUtf8(buffer), typed); - GFSecFreeMemory(buffer); + GFMemFree(GFModuleSdkContext(), GF_ARENA_SECURE, buffer); } } // namespace |
