/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #ifndef mozilla_dom_SyncedContextInlines_h #define mozilla_dom_SyncedContextInlines_h #include "mozilla/dom/SyncedContext.h" #include "mozilla/dom/BrowsingContextGroup.h" #include "mozilla/dom/ContentParent.h" #include "mozilla/dom/ContentChild.h" #include "nsReadableUtils.h" #include "mozilla/HalIPCUtils.h" namespace mozilla { namespace dom { namespace syncedcontext { template struct IsMozillaMaybe : std::false_type {}; template struct IsMozillaMaybe> : std::true_type {}; // A super-sketchy logging only function for generating a human-readable version // of the value of a synced context field. template void FormatFieldValue(nsACString& aStr, const T& aValue) { if constexpr (std::is_same_v) { if (aValue) { aStr.AppendLiteral("true"); } else { aStr.AppendLiteral("false"); } } else if constexpr (std::is_same_v) { aStr.Append(nsIDToCString(aValue).get()); } else if constexpr (std::is_integral_v) { aStr.AppendInt(aValue); } else if constexpr (std::is_enum_v) { aStr.AppendInt(static_cast>(aValue)); } else if constexpr (std::is_floating_point_v) { aStr.AppendFloat(aValue); } else if constexpr (std::is_base_of_v) { AppendUTF16toUTF8(aValue, aStr); } else if constexpr (std::is_base_of_v) { aStr.Append(aValue); } else if constexpr (IsMozillaMaybe::value) { if (aValue) { aStr.AppendLiteral("Some("); FormatFieldValue(aStr, aValue.ref()); aStr.AppendLiteral(")"); } else { aStr.AppendLiteral("Nothing"); } } else { aStr.AppendLiteral("???"); } } // Sketchy logging-only logic to generate a human-readable output of the actions // a synced context transaction is going to perform. template nsAutoCString FormatTransaction( typename Transaction::IndexSet aModified, const typename Context::FieldValues& aOldValues, const typename Context::FieldValues& aNewValues) { nsAutoCString result; Context::FieldValues::EachIndex([&](auto idx) { if (aModified.contains(idx)) { result.Append(Context::FieldIndexToName(idx)); result.AppendLiteral("("); FormatFieldValue(result, aOldValues.Get(idx)); result.AppendLiteral("->"); FormatFieldValue(result, aNewValues.Get(idx)); result.AppendLiteral(") "); } }); return result; } template nsCString FormatValidationError( typename Transaction::IndexSet aFailedFields, const char* prefix) { MOZ_ASSERT(!aFailedFields.isEmpty()); return nsDependentCString{prefix} + StringJoin(", "_ns, aFailedFields, [](nsACString& dest, const auto& idx) { dest.Append(Context::FieldIndexToName(idx)); }); } template nsresult Transaction::Commit(Context* aOwner) { if (NS_WARN_IF(aOwner->IsDiscarded())) { return NS_ERROR_DOM_INVALID_STATE_ERR; } IndexSet failedFields = Validate(aOwner, nullptr); if (!failedFields.isEmpty()) { nsCString error = FormatValidationError( failedFields, "CanSet failed for field(s): "); MOZ_CRASH_UNSAFE_PRINTF("%s", error.get()); } if (mModified.isEmpty()) { return NS_OK; } if (XRE_IsContentProcess()) { ContentChild* cc = ContentChild::GetSingleton(); // Increment the field epoch for fields affected by this transaction. uint64_t epoch = cc->NextBrowsingContextFieldEpoch(); EachIndex([&](auto idx) { if (mModified.contains(idx)) { FieldEpoch(idx, aOwner) = epoch; } }); // Tell our derived class to send the correct "Commit" IPC message. aOwner->SendCommitTransaction(cc, *this, epoch); } else { MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess()); // Tell our derived class to send the correct "Commit" IPC messages. BrowsingContextGroup* group = aOwner->Group(); group->EachParent([&](ContentParent* aParent) { aOwner->SendCommitTransaction(aParent, *this, aParent->GetBrowsingContextFieldEpoch()); }); } Apply(aOwner, /* aFromIPC */ false); return NS_OK; } template mozilla::ipc::IPCResult Transaction::CommitFromIPC( const MaybeDiscarded& aOwner, ContentParent* aSource) { MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess()); if (aOwner.IsNullOrDiscarded()) { MOZ_LOG(Context::GetSyncLog(), LogLevel::Debug, ("IPC: Trying to send a message to dead or detached context")); return IPC_OK(); } Context* owner = aOwner.get(); // Validate that the set from content is allowed before continuing. IndexSet failedFields = Validate(owner, aSource); if (!failedFields.isEmpty()) { nsCString error = FormatValidationError( failedFields, "Invalid Transaction from Child - CanSet failed for field(s): "); return IPC_FAIL(aSource, error.get()); } // Validate may have dropped some fields from the transaction, check it's not // empty before continuing. if (mModified.isEmpty()) { return IPC_OK(); } BrowsingContextGroup* group = owner->Group(); group->EachOtherParent(aSource, [&](ContentParent* aParent) { owner->SendCommitTransaction(aParent, *this, aParent->GetBrowsingContextFieldEpoch()); }); Apply(owner, /* aFromIPC */ true); return IPC_OK(); } template mozilla::ipc::IPCResult Transaction::CommitFromIPC( const MaybeDiscarded& aOwner, uint64_t aEpoch, ContentChild* aSource) { MOZ_DIAGNOSTIC_ASSERT(XRE_IsContentProcess()); if (aOwner.IsNullOrDiscarded()) { MOZ_LOG(Context::GetSyncLog(), LogLevel::Debug, ("ChildIPC: Trying to send a message to dead or detached context")); return IPC_OK(); } Context* owner = aOwner.get(); // Clear any fields which have been obsoleted by the epoch. EachIndex([&](auto idx) { if (mModified.contains(idx) && FieldEpoch(idx, owner) > aEpoch) { MOZ_LOG( Context::GetSyncLog(), LogLevel::Debug, ("Transaction::Obsoleted(#%" PRIx64 ", %" PRIu64 ">%" PRIu64 "): %s", owner->Id(), FieldEpoch(idx, owner), aEpoch, Context::FieldIndexToName(idx))); mModified -= idx; } }); if (mModified.isEmpty()) { return IPC_OK(); } Apply(owner, /* aFromIPC */ true); return IPC_OK(); } template void Transaction::Apply(Context* aOwner, bool aFromIPC) { MOZ_ASSERT(!mModified.isEmpty()); MOZ_LOG( Context::GetSyncLog(), LogLevel::Debug, ("Transaction::Apply(#%" PRIx64 ", %s): %s", aOwner->Id(), aFromIPC ? "ipc" : "local", FormatTransaction(mModified, aOwner->mFields.mValues, mValues) .get())); EachIndex([&](auto idx) { if (mModified.contains(idx)) { auto& txnField = mValues.Get(idx); auto& ownerField = aOwner->mFields.mValues.Get(idx); std::swap(ownerField, txnField); aOwner->DidSet(idx); aOwner->DidSet(idx, std::move(txnField)); } }); mModified.clear(); } template void Transaction::CommitWithoutSyncing(Context* aOwner) { MOZ_LOG( Context::GetSyncLog(), LogLevel::Debug, ("Transaction::CommitWithoutSyncing(#%" PRIx64 "): %s", aOwner->Id(), FormatTransaction(mModified, aOwner->mFields.mValues, mValues) .get())); EachIndex([&](auto idx) { if (mModified.contains(idx)) { aOwner->mFields.mValues.Get(idx) = std::move(mValues.Get(idx)); } }); mModified.clear(); } inline CanSetResult AsCanSetResult(CanSetResult aValue) { return aValue; } inline CanSetResult AsCanSetResult(bool aValue) { return aValue ? CanSetResult::Allow : CanSetResult::Deny; } template typename Transaction::IndexSet Transaction::Validate( Context* aOwner, ContentParent* aSource) { IndexSet failedFields; Transaction revertTxn; EachIndex([&](auto idx) { if (!mModified.contains(idx)) { return; } switch (AsCanSetResult(aOwner->CanSet(idx, mValues.Get(idx), aSource))) { case CanSetResult::Allow: break; case CanSetResult::Deny: failedFields += idx; break; case CanSetResult::Revert: revertTxn.mValues.Get(idx) = aOwner->mFields.mValues.Get(idx); revertTxn.mModified += idx; break; } }); // If any changes need to be reverted, log them, remove them from this // transaction, and optionally send a message to the source process. if (!revertTxn.mModified.isEmpty()) { // NOTE: Logging with modified IndexSet from revert transaction, and values // from this transaction, so we log the failed values we're going to revert. MOZ_LOG( Context::GetSyncLog(), LogLevel::Debug, ("Transaction::PartialRevert(#%" PRIx64 ", pid %" PRIPID "): %s", aOwner->Id(), aSource ? aSource->OtherPid() : base::kInvalidProcessId, FormatTransaction(revertTxn.mModified, mValues, revertTxn.mValues) .get())); mModified -= revertTxn.mModified; if (aSource) { aOwner->SendCommitTransaction(aSource, revertTxn, aSource->GetBrowsingContextFieldEpoch()); } } return failedFields; } template void Transaction::Write(IPC::MessageWriter* aWriter, mozilla::ipc::IProtocol* aActor) const { // Record which field indices will be included, and then write those fields // out. typename IndexSet::serializedType modified = mModified.serialize(); WriteIPDLParam(aWriter, aActor, modified); EachIndex([&](auto idx) { if (mModified.contains(idx)) { WriteIPDLParam(aWriter, aActor, mValues.Get(idx)); } }); } template bool Transaction::Read(IPC::MessageReader* aReader, mozilla::ipc::IProtocol* aActor) { // Read in which field indices were sent by the remote, followed by the fields // identified by those indices. typename IndexSet::serializedType modified = typename IndexSet::serializedType{}; if (!ReadIPDLParam(aReader, aActor, &modified)) { return false; } mModified.deserialize(modified); bool ok = true; EachIndex([&](auto idx) { if (ok && mModified.contains(idx)) { ok = ReadIPDLParam(aReader, aActor, &mValues.Get(idx)); } }); return ok; } template void FieldValues::Write(IPC::MessageWriter* aWriter, mozilla::ipc::IProtocol* aActor) const { // XXX The this-> qualification is necessary to work around a bug in older gcc // versions causing an ICE. EachIndex([&](auto idx) { WriteIPDLParam(aWriter, aActor, this->Get(idx)); }); } template bool FieldValues::Read(IPC::MessageReader* aReader, mozilla::ipc::IProtocol* aActor) { bool ok = true; EachIndex([&](auto idx) { if (ok) { // XXX The this-> qualification is necessary to work around a bug in older // gcc versions causing an ICE. ok = ReadIPDLParam(aReader, aActor, &this->Get(idx)); } }); return ok; } } // namespace syncedcontext } // namespace dom } // namespace mozilla #endif // !defined(mozilla_dom_SyncedContextInlines_h)