/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set sw=2 ts=2 et 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/. */ #include "nsHtml5Parser.h" #include "mozilla/AutoRestore.h" #include "nsCRT.h" #include "nsContentUtils.h" // for kLoadAsData #include "nsHtml5AtomTable.h" #include "nsHtml5DependentUTF16Buffer.h" #include "nsHtml5Tokenizer.h" #include "nsHtml5TreeBuilder.h" #include "nsNetUtil.h" NS_INTERFACE_TABLE_HEAD(nsHtml5Parser) NS_INTERFACE_TABLE(nsHtml5Parser, nsIParser, nsISupportsWeakReference) NS_INTERFACE_TABLE_TO_MAP_SEGUE_CYCLE_COLLECTION(nsHtml5Parser) NS_INTERFACE_MAP_END NS_IMPL_CYCLE_COLLECTING_ADDREF(nsHtml5Parser) NS_IMPL_CYCLE_COLLECTING_RELEASE(nsHtml5Parser) NS_IMPL_CYCLE_COLLECTION_CLASS(nsHtml5Parser) NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(nsHtml5Parser) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mExecutor) NS_IMPL_CYCLE_COLLECTION_TRAVERSE_RAWPTR(GetStreamParser()) NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(nsHtml5Parser) NS_IMPL_CYCLE_COLLECTION_UNLINK(mExecutor) NS_IMPL_CYCLE_COLLECTION_UNLINK_WEAK_REFERENCE tmp->DropStreamParser(); NS_IMPL_CYCLE_COLLECTION_UNLINK_END nsHtml5Parser::nsHtml5Parser() : mLastWasCR(false), mDocWriteSpeculativeLastWasCR(false), mBlocked(0), mDocWriteSpeculatorActive(false), mScriptNestingLevel(0), mDocumentClosed(false), mInDocumentWrite(false), mInsertionPointPermanentlyUndefined(false), mFirstBuffer(new nsHtml5OwningUTF16Buffer((void*)nullptr)), mLastBuffer(mFirstBuffer), mExecutor(new nsHtml5TreeOpExecutor()), mTreeBuilder(new nsHtml5TreeBuilder(mExecutor, nullptr, false)), mTokenizer(new nsHtml5Tokenizer(mTreeBuilder.get(), false)), mRootContextLineNumber(1), mReturnToStreamParserPermitted(false) { mTokenizer->setInterner(&mAtomTable); } nsHtml5Parser::~nsHtml5Parser() { mTokenizer->end(); if (mDocWriteSpeculativeTokenizer) { mDocWriteSpeculativeTokenizer->end(); } } NS_IMETHODIMP_(void) nsHtml5Parser::SetContentSink(nsIContentSink* aSink) { NS_ASSERTION(aSink == static_cast(mExecutor), "Attempt to set a foreign sink."); } NS_IMETHODIMP_(nsIContentSink*) nsHtml5Parser::GetContentSink() { return static_cast(mExecutor); } NS_IMETHODIMP_(void) nsHtml5Parser::GetCommand(nsCString& aCommand) { aCommand.AssignLiteral("view"); } NS_IMETHODIMP_(void) nsHtml5Parser::SetCommand(const char* aCommand) { NS_ASSERTION(!strcmp(aCommand, "view") || !strcmp(aCommand, "view-source") || !strcmp(aCommand, "external-resource") || !strcmp(aCommand, "import") || !strcmp(aCommand, kLoadAsData), "Unsupported parser command"); } NS_IMETHODIMP_(void) nsHtml5Parser::SetCommand(eParserCommands aParserCommand) { NS_ASSERTION(aParserCommand == eViewNormal, "Parser command was not eViewNormal."); } void nsHtml5Parser::SetDocumentCharset(NotNull aEncoding, int32_t aCharsetSource, bool aForceAutoDetection) { MOZ_ASSERT(!mExecutor->HasStarted(), "Document charset set too late."); MOZ_ASSERT(GetStreamParser(), "Setting charset on a script-only parser."); GetStreamParser()->SetDocumentCharset( aEncoding, (nsCharsetSource)aCharsetSource, aForceAutoDetection); mExecutor->SetDocumentCharsetAndSource(aEncoding, (nsCharsetSource)aCharsetSource); } nsresult nsHtml5Parser::GetChannel(nsIChannel** aChannel) { if (GetStreamParser()) { return GetStreamParser()->GetChannel(aChannel); } else { return NS_ERROR_NOT_AVAILABLE; } } nsIStreamListener* nsHtml5Parser::GetStreamListener() { return mStreamListener; } NS_IMETHODIMP nsHtml5Parser::ContinueInterruptedParsing() { MOZ_ASSERT_UNREACHABLE("Don't call. For interface compat only."); return NS_ERROR_NOT_IMPLEMENTED; } NS_IMETHODIMP_(void) nsHtml5Parser::BlockParser() { mBlocked++; } NS_IMETHODIMP_(void) nsHtml5Parser::UnblockParser() { MOZ_DIAGNOSTIC_ASSERT(mBlocked > 0); if (MOZ_LIKELY(mBlocked > 0)) { mBlocked--; } if (MOZ_LIKELY(mBlocked == 0) && mExecutor) { mExecutor->ContinueInterruptedParsingAsync(); } } NS_IMETHODIMP_(void) nsHtml5Parser::ContinueInterruptedParsingAsync() { if (mExecutor) { mExecutor->ContinueInterruptedParsingAsync(); } } NS_IMETHODIMP_(bool) nsHtml5Parser::IsParserEnabled() { return !mBlocked; } NS_IMETHODIMP_(bool) nsHtml5Parser::IsComplete() { return mExecutor->IsComplete(); } NS_IMETHODIMP nsHtml5Parser::Parse(nsIURI* aURL) { /* * Do NOT cause WillBuildModel to be called synchronously from here! * The document won't be ready for it until OnStartRequest! */ MOZ_ASSERT(!mExecutor->HasStarted(), "Tried to start parse without initializing the parser."); MOZ_ASSERT(GetStreamParser(), "Can't call this Parse() variant on script-created parser"); GetStreamParser()->SetViewSourceTitle(aURL); // In case we're viewing source mExecutor->SetStreamParser(GetStreamParser()); mExecutor->SetParser(this); return NS_OK; } nsresult nsHtml5Parser::Parse(const nsAString& aSourceBuffer, void* aKey, bool aLastCall) { nsresult rv; if (NS_FAILED(rv = mExecutor->IsBroken())) { return rv; } if (aSourceBuffer.Length() > INT32_MAX) { return mExecutor->MarkAsBroken(NS_ERROR_OUT_OF_MEMORY); } // Maintain a reference to ourselves so we don't go away // till we're completely done. The old parser grips itself in this method. nsCOMPtr kungFuDeathGrip(this); // Gripping the other objects just in case, since the other old grip // required grips to these, too. RefPtr streamKungFuDeathGrip(GetStreamParser()); mozilla::Unused << streamKungFuDeathGrip; // Not used within function RefPtr executor(mExecutor); MOZ_RELEASE_ASSERT(executor->HasStarted()); // Return early if the parser has processed EOF if (executor->IsComplete()) { return NS_OK; } if (aLastCall && aSourceBuffer.IsEmpty() && !aKey) { // document.close() NS_ASSERTION(!GetStreamParser(), "Had stream parser but got document.close()."); if (mDocumentClosed) { // already closed return NS_OK; } mDocumentClosed = true; if (!mBlocked && !mInDocumentWrite) { return ParseUntilBlocked(); } return NS_OK; } // If we got this far, we are dealing with a document.write or // document.writeln call--not document.close(). MOZ_RELEASE_ASSERT( IsInsertionPointDefined(), "Doc.write reached parser with undefined insertion point."); MOZ_RELEASE_ASSERT(!(GetStreamParser() && !aKey), "Got a null key in a non-script-created parser"); // XXX is this optimization bogus? if (aSourceBuffer.IsEmpty()) { return NS_OK; } // This guard is here to prevent document.close from tokenizing synchronously // while a document.write (that wrote the script that called document.close!) // is still on the call stack. mozilla::AutoRestore guard(mInDocumentWrite); mInDocumentWrite = true; // The script is identified by aKey. If there's nothing in the buffer // chain for that key, we'll insert at the head of the queue. // When the script leaves something in the queue, a zero-length // key-holder "buffer" is inserted in the queue. If the same script // leaves something in the chain again, it will be inserted immediately // before the old key holder belonging to the same script. // // We don't do the actual data insertion yet in the hope that the data gets // tokenized and there no data or less data to copy to the heap after // tokenization. Also, this way, we avoid inserting one empty data buffer // per document.write, which matters for performance when the parser isn't // blocked and a badly-authored script calls document.write() once per // input character. (As seen in a benchmark!) // // The insertion into the input stream happens conceptually before anything // gets tokenized. To make sure multi-level document.write works right, // it's necessary to establish the location of our parser key up front // in case this is the first write with this key. // // In a document.open() case, the first write level has a null key, so that // case is handled separately, because normal buffers containing data // have null keys. // These don't need to be owning references, because they always point to // the buffer queue and buffers can't be removed from the buffer queue // before document.write() returns. The buffer queue clean-up happens the // next time ParseUntilBlocked() is called. // However, they are made owning just in case the reasoning above is flawed // and a flaw would lead to worse problems with plain pointers. If this // turns out to be a perf problem, it's worthwhile to consider making // prevSearchbuf a plain pointer again. RefPtr prevSearchBuf; RefPtr firstLevelMarker; if (aKey) { if (mFirstBuffer == mLastBuffer) { nsHtml5OwningUTF16Buffer* keyHolder = new nsHtml5OwningUTF16Buffer(aKey); keyHolder->next = mLastBuffer; mFirstBuffer = keyHolder; } else if (mFirstBuffer->key != aKey) { prevSearchBuf = mFirstBuffer; for (;;) { if (prevSearchBuf->next == mLastBuffer) { // key was not found nsHtml5OwningUTF16Buffer* keyHolder = new nsHtml5OwningUTF16Buffer(aKey); keyHolder->next = mFirstBuffer; mFirstBuffer = keyHolder; prevSearchBuf = nullptr; break; } if (prevSearchBuf->next->key == aKey) { // found a key holder break; } prevSearchBuf = prevSearchBuf->next; } } // else mFirstBuffer is the keyholder // prevSearchBuf is the previous buffer before the keyholder or null if // there isn't one. } else { // We have a first-level write in the document.open() case. We insert before // mLastBuffer, effectively, by making mLastBuffer be a new sentinel object // and redesignating the previous mLastBuffer as our firstLevelMarker. We // need to put a marker there, because otherwise additional document.writes // from nested event loops would insert in the wrong place. Sigh. mLastBuffer->next = new nsHtml5OwningUTF16Buffer((void*)nullptr); firstLevelMarker = mLastBuffer; mLastBuffer = mLastBuffer->next; } nsHtml5DependentUTF16Buffer stackBuffer(aSourceBuffer); while (!mBlocked && stackBuffer.hasMore()) { stackBuffer.adjust(mLastWasCR); mLastWasCR = false; if (stackBuffer.hasMore()) { int32_t lineNumberSave; bool inRootContext = (!GetStreamParser() && !aKey); if (inRootContext) { mTokenizer->setLineNumber(mRootContextLineNumber); } else { // we aren't the root context, so save the line number on the // *stack* so that we can restore it. lineNumberSave = mTokenizer->getLineNumber(); } if (!mTokenizer->EnsureBufferSpace(stackBuffer.getLength())) { return executor->MarkAsBroken(NS_ERROR_OUT_OF_MEMORY); } mLastWasCR = mTokenizer->tokenizeBuffer(&stackBuffer); if (NS_FAILED((rv = mTreeBuilder->IsBroken()))) { return executor->MarkAsBroken(rv); } if (inRootContext) { mRootContextLineNumber = mTokenizer->getLineNumber(); } else { mTokenizer->setLineNumber(lineNumberSave); } if (mTreeBuilder->HasScript()) { auto r = mTreeBuilder->Flush(); // Move ops to the executor if (r.isErr()) { return executor->MarkAsBroken(r.unwrapErr()); } rv = executor->FlushDocumentWrite(); // run the ops NS_ENSURE_SUCCESS(rv, rv); // Flushing tree ops can cause all sorts of things. // Return early if the parser got terminated. if (executor->IsComplete()) { return NS_OK; } } // Ignore suspension requests } } RefPtr heapBuffer; if (stackBuffer.hasMore()) { // The buffer wasn't tokenized to completion. Create a copy of the tail // on the heap. heapBuffer = stackBuffer.FalliblyCopyAsOwningBuffer(); if (!heapBuffer) { // Allocation failed. The parser is now broken. return executor->MarkAsBroken(NS_ERROR_OUT_OF_MEMORY); } } if (heapBuffer) { // We have something to insert before the keyholder holding in the non-null // aKey case and we have something to swap into firstLevelMarker in the // null aKey case. if (aKey) { NS_ASSERTION(mFirstBuffer != mLastBuffer, "Where's the keyholder?"); // the key holder is still somewhere further down the list from // prevSearchBuf (which may be null) if (mFirstBuffer->key == aKey) { NS_ASSERTION( !prevSearchBuf, "Non-null prevSearchBuf when mFirstBuffer is the key holder?"); heapBuffer->next = mFirstBuffer; mFirstBuffer = heapBuffer; } else { if (!prevSearchBuf) { prevSearchBuf = mFirstBuffer; } // We created a key holder earlier, so we will find it without walking // past the end of the list. while (prevSearchBuf->next->key != aKey) { prevSearchBuf = prevSearchBuf->next; } heapBuffer->next = prevSearchBuf->next; prevSearchBuf->next = heapBuffer; } } else { NS_ASSERTION(firstLevelMarker, "How come we don't have a marker."); firstLevelMarker->Swap(heapBuffer); } } if (!mBlocked) { // buffer was tokenized to completion NS_ASSERTION(!stackBuffer.hasMore(), "Buffer wasn't tokenized to completion?"); // Scripting semantics require a forced tree builder flush here auto r = mTreeBuilder->Flush(); // Move ops to the executor if (r.isErr()) { return executor->MarkAsBroken(r.unwrapErr()); } rv = executor->FlushDocumentWrite(); // run the ops NS_ENSURE_SUCCESS(rv, rv); } else if (stackBuffer.hasMore()) { // The buffer wasn't tokenized to completion. Tokenize the untokenized // content in order to preload stuff. This content will be retokenized // later for normal parsing. if (!mDocWriteSpeculatorActive) { mDocWriteSpeculatorActive = true; if (!mDocWriteSpeculativeTreeBuilder) { // Lazily initialize if uninitialized mDocWriteSpeculativeTreeBuilder = MakeUnique(nullptr, executor->GetStage(), true); mDocWriteSpeculativeTreeBuilder->setScriptingEnabled( mTreeBuilder->isScriptingEnabled()); mDocWriteSpeculativeTokenizer = MakeUnique( mDocWriteSpeculativeTreeBuilder.get(), false); mDocWriteSpeculativeTokenizer->setInterner(&mAtomTable); mDocWriteSpeculativeTokenizer->start(); } mDocWriteSpeculativeTokenizer->resetToDataState(); mDocWriteSpeculativeTreeBuilder->loadState(mTreeBuilder.get()); mDocWriteSpeculativeLastWasCR = false; } // Note that with multilevel document.write if we didn't just activate the // speculator, it's possible that the speculator is now in the wrong state. // That's OK for the sake of simplicity. The worst that can happen is // that the speculative loads aren't exactly right. The content will be // reparsed anyway for non-preload purposes. // The buffer position for subsequent non-speculative parsing now lives // in heapBuffer, so it's ok to let the buffer position of stackBuffer // to be overwritten and not restored below. while (stackBuffer.hasMore()) { stackBuffer.adjust(mDocWriteSpeculativeLastWasCR); if (stackBuffer.hasMore()) { if (!mDocWriteSpeculativeTokenizer->EnsureBufferSpace( stackBuffer.getLength())) { return executor->MarkAsBroken(NS_ERROR_OUT_OF_MEMORY); } mDocWriteSpeculativeLastWasCR = mDocWriteSpeculativeTokenizer->tokenizeBuffer(&stackBuffer); nsresult rv; if (NS_FAILED((rv = mDocWriteSpeculativeTreeBuilder->IsBroken()))) { return executor->MarkAsBroken(rv); } } } auto r = mDocWriteSpeculativeTreeBuilder->Flush(); if (r.isErr()) { return executor->MarkAsBroken(r.unwrapErr()); } mDocWriteSpeculativeTreeBuilder->DropHandles(); executor->FlushSpeculativeLoads(); } return NS_OK; } NS_IMETHODIMP nsHtml5Parser::Terminate() { // Prevent a second call to DidBuildModel via document.close() mDocumentClosed = true; // We should only call DidBuildModel once, so don't do anything if this is // the second time that Terminate has been called. if (mExecutor->IsComplete()) { return NS_OK; } // XXX - [ until we figure out a way to break parser-sink circularity ] // Hack - Hold a reference until we are completely done... nsCOMPtr kungFuDeathGrip(this); RefPtr streamParser(GetStreamParser()); RefPtr executor(mExecutor); if (streamParser) { streamParser->Terminate(); } return executor->DidBuildModel(true); } bool nsHtml5Parser::IsInsertionPointDefined() { return !mExecutor->IsFlushing() && !mInsertionPointPermanentlyUndefined && (!GetStreamParser() || mScriptNestingLevel != 0); } void nsHtml5Parser::IncrementScriptNestingLevel() { ++mScriptNestingLevel; } void nsHtml5Parser::DecrementScriptNestingLevel() { --mScriptNestingLevel; } bool nsHtml5Parser::HasNonzeroScriptNestingLevel() const { return mScriptNestingLevel != 0; } void nsHtml5Parser::MarkAsNotScriptCreated(const char* aCommand) { MOZ_ASSERT(!mStreamListener, "Must not call this twice."); eParserMode mode = NORMAL; if (!nsCRT::strcmp(aCommand, "view-source")) { mode = VIEW_SOURCE_HTML; } else if (!nsCRT::strcmp(aCommand, "view-source-xml")) { mode = VIEW_SOURCE_XML; } else if (!nsCRT::strcmp(aCommand, "view-source-plain")) { mode = VIEW_SOURCE_PLAIN; } else if (!nsCRT::strcmp(aCommand, "plain-text")) { mode = PLAIN_TEXT; } else if (!nsCRT::strcmp(aCommand, kLoadAsData)) { mode = LOAD_AS_DATA; } #ifdef DEBUG else { NS_ASSERTION(!nsCRT::strcmp(aCommand, "view") || !nsCRT::strcmp(aCommand, "external-resource") || !nsCRT::strcmp(aCommand, "import"), "Unsupported parser command!"); } #endif mStreamListener = new nsHtml5StreamListener(new nsHtml5StreamParser(mExecutor, this, mode)); } bool nsHtml5Parser::IsScriptCreated() { return !GetStreamParser(); } /* End nsIParser */ // not from interface nsresult nsHtml5Parser::ParseUntilBlocked() { nsresult rv = mExecutor->IsBroken(); NS_ENSURE_SUCCESS(rv, rv); if (mBlocked || mInsertionPointPermanentlyUndefined || mExecutor->IsComplete()) { return NS_OK; } NS_ASSERTION(mExecutor->HasStarted(), "Bad life cycle."); NS_ASSERTION(!mInDocumentWrite, "ParseUntilBlocked entered while in doc.write!"); mDocWriteSpeculatorActive = false; for (;;) { if (!mFirstBuffer->hasMore()) { if (mFirstBuffer == mLastBuffer) { if (mExecutor->IsComplete()) { // something like cache manisfests stopped the parse in mid-flight return NS_OK; } if (mDocumentClosed) { PermanentlyUndefineInsertionPoint(); nsresult rv; MOZ_RELEASE_ASSERT( !GetStreamParser(), "This should only happen with script-created parser."); if (NS_SUCCEEDED((rv = mExecutor->IsBroken()))) { mTokenizer->eof(); if (NS_FAILED((rv = mTreeBuilder->IsBroken()))) { mExecutor->MarkAsBroken(rv); } else { mTreeBuilder->StreamEnded(); } } auto r = mTreeBuilder->Flush(); if (r.isErr()) { return mExecutor->MarkAsBroken(r.unwrapErr()); } mExecutor->FlushDocumentWrite(); // The below call does memory cleanup, so call it even if the // parser has been marked as broken. mTokenizer->end(); return rv; } // never release the last buffer. NS_ASSERTION(!mLastBuffer->getStart() && !mLastBuffer->getEnd(), "Sentinel buffer had its indeces changed."); if (GetStreamParser()) { if (mReturnToStreamParserPermitted && !mExecutor->IsScriptExecuting()) { auto r = mTreeBuilder->Flush(); if (r.isErr()) { return mExecutor->MarkAsBroken(r.unwrapErr()); } mReturnToStreamParserPermitted = false; GetStreamParser()->ContinueAfterScriptsOrEncodingCommitment( mTokenizer.get(), mTreeBuilder.get(), mLastWasCR); } } else { // Script-created parser auto r = mTreeBuilder->Flush(); if (r.isErr()) { return mExecutor->MarkAsBroken(r.unwrapErr()); } // No need to flush the executor, because the executor is already // in a flush NS_ASSERTION(mExecutor->IsInFlushLoop(), "How did we come here without being in the flush loop?"); } return NS_OK; // no more data for now but expecting more } mFirstBuffer = mFirstBuffer->next; continue; } if (mBlocked || mExecutor->IsComplete()) { return NS_OK; } // now we have a non-empty buffer mFirstBuffer->adjust(mLastWasCR); mLastWasCR = false; if (mFirstBuffer->hasMore()) { bool inRootContext = (!GetStreamParser() && !mFirstBuffer->key); if (inRootContext) { mTokenizer->setLineNumber(mRootContextLineNumber); } if (!mTokenizer->EnsureBufferSpace(mFirstBuffer->getLength())) { return mExecutor->MarkAsBroken(NS_ERROR_OUT_OF_MEMORY); } mLastWasCR = mTokenizer->tokenizeBuffer(mFirstBuffer); nsresult rv; if (NS_FAILED((rv = mTreeBuilder->IsBroken()))) { return mExecutor->MarkAsBroken(rv); } if (inRootContext) { mRootContextLineNumber = mTokenizer->getLineNumber(); } if (mTreeBuilder->HasScript()) { auto r = mTreeBuilder->Flush(); if (r.isErr()) { return mExecutor->MarkAsBroken(r.unwrapErr()); } rv = mExecutor->FlushDocumentWrite(); NS_ENSURE_SUCCESS(rv, rv); } if (mBlocked) { return NS_OK; } } } } nsresult nsHtml5Parser::StartExecutor() { MOZ_ASSERT(!GetStreamParser(), "Had stream parser but document.write started life cycle."); // This is part of the setup document.open() does. RefPtr executor(mExecutor); executor->SetParser(this); mTreeBuilder->setScriptingEnabled(executor->IsScriptEnabled()); mTreeBuilder->setIsSrcdocDocument(false); mTokenizer->start(); executor->Start(); /* * We know we're in document.open(), so our document must already * have a script global andthe WillBuildModel call is safe. */ return executor->WillBuildModel(); } nsresult nsHtml5Parser::Initialize(mozilla::dom::Document* aDoc, nsIURI* aURI, nsISupports* aContainer, nsIChannel* aChannel) { return mExecutor->Init(aDoc, aURI, aContainer, aChannel); } void nsHtml5Parser::StartTokenizer(bool aScriptingEnabled) { bool isSrcdoc = false; nsCOMPtr channel; nsresult rv = GetChannel(getter_AddRefs(channel)); if (NS_SUCCEEDED(rv)) { isSrcdoc = NS_IsSrcdocChannel(channel); } mTreeBuilder->setIsSrcdocDocument(isSrcdoc); mTreeBuilder->SetPreventScriptExecution(!aScriptingEnabled); mTreeBuilder->setScriptingEnabled(aScriptingEnabled); mTokenizer->start(); } void nsHtml5Parser::InitializeDocWriteParserState( nsAHtml5TreeBuilderState* aState, int32_t aLine) { mTokenizer->resetToDataState(); mTokenizer->setLineNumber(aLine); mTreeBuilder->loadState(aState); mLastWasCR = false; mReturnToStreamParserPermitted = true; } void nsHtml5Parser::ContinueAfterFailedCharsetSwitch() { MOZ_ASSERT( GetStreamParser(), "Tried to continue after failed charset switch without a stream parser"); GetStreamParser()->ContinueAfterFailedCharsetSwitch(); }