xpcom/io/SnappyUncompressInputStream.cpp
author Jan Henning <jh+bugzilla@buttercookie.de>
Thu, 07 Feb 2019 20:41:16 +0000
changeset 512996 95b884dcbd6f001b990a6353980280050fab9ac6
parent 505383 6f3709b3878117466168c40affa7bca0b60cf75b
permissions -rw-r--r--
Bug 1496684 - Dispatch commonly expected startup notifications when opening a GeckoView window. r=snorp a=lizzard Once a webextension using a blocking WebRequest listener has started loading, all network connections covered by the extension's manifest are held until the extension is ready the process them. One condition for the extension being ready apparently includes browser startup having progressed far enough, as signified by "browser-delayed-startup-finished" having been dispatched. Therefore, we have to start sending that notification when opening a new Gecko- View window, too, and copy Fennec's InitLater() system for that. Unlike Fennec, we cannot tie registration of those InitLater() runnables to the initial content load having progressed far enough because of a) e10s, which makes that approach neither easily possible nor really sensible, as content will load in a different process in that case, and b) because we're racing with extension startup here - if extensions are loaded quick enough to block even the initial page load, we'd be deadlocked: We cannot send the notification until the page finishes loading, but the page cannot load until we send the notification. Fennec isn't affected by the latter problem because "sessionstore-windows-restored", which Fennec will send in any case, serves as an alternative pathway for completing extension startup. And unlike Desktop, we don't really have any chrome content to paint, so we cannot tie delayed initialisation to a paint listener for that, either. Therefore, we simply fire off a runnable at the *end* of geckoview.js's startup() method, which should give more pressing initialisation tasks enough of a headstart. For completeness, we're also adding the "browser-idle-startup-tasks-finished" notification and thereby solve bug 1465832 as well, allowing the ScriptPreloader to detect which scripts are commonly loaded during GeckoView startup and to start caching and pre-parsing them. Differential Revision: https://phabricator.services.mozilla.com/D18865

/* -*- 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/. */

#include "mozilla/SnappyUncompressInputStream.h"

#include <algorithm>
#include "nsIAsyncInputStream.h"
#include "nsStreamUtils.h"
#include "snappy/snappy.h"

namespace mozilla {

NS_IMPL_ISUPPORTS(SnappyUncompressInputStream, nsIInputStream);

// Putting kCompressedBufferLength inside a function avoids a static
// constructor.
static size_t CompressedBufferLength() {
  static size_t kCompressedBufferLength =
      detail::SnappyFrameUtils::MaxCompressedBufferLength(snappy::kBlockSize);

  MOZ_ASSERT(kCompressedBufferLength > 0);
  return kCompressedBufferLength;
}

SnappyUncompressInputStream::SnappyUncompressInputStream(
    nsIInputStream* aBaseStream)
    : mBaseStream(aBaseStream),
      mUncompressedBytes(0),
      mNextByte(0),
      mNextChunkType(Unknown),
      mNextChunkDataLength(0),
      mNeedFirstStreamIdentifier(true) {
  // This implementation only supports sync base streams.  Verify this in debug
  // builds.  Note, this is a bit complicated because the streams we support
  // advertise different capabilities:
  //  - nsFileInputStream - blocking and sync
  //  - nsStringInputStream - non-blocking and sync
  //  - nsPipeInputStream - can be blocking, but provides async interface
#ifdef DEBUG
  bool baseNonBlocking;
  nsresult rv = mBaseStream->IsNonBlocking(&baseNonBlocking);
  MOZ_ASSERT(NS_SUCCEEDED(rv));
  if (baseNonBlocking) {
    nsCOMPtr<nsIAsyncInputStream> async = do_QueryInterface(mBaseStream);
    MOZ_ASSERT(!async);
  }
#endif
}

NS_IMETHODIMP
SnappyUncompressInputStream::Close() {
  if (!mBaseStream) {
    return NS_OK;
  }

  mBaseStream->Close();
  mBaseStream = nullptr;

  mUncompressedBuffer = nullptr;
  mCompressedBuffer = nullptr;

  return NS_OK;
}

NS_IMETHODIMP
SnappyUncompressInputStream::Available(uint64_t* aLengthOut) {
  if (!mBaseStream) {
    return NS_BASE_STREAM_CLOSED;
  }

  // If we have uncompressed bytes, then we are done.
  *aLengthOut = UncompressedLength();
  if (*aLengthOut > 0) {
    return NS_OK;
  }

  // Otherwise, attempt to uncompress bytes until we get something or the
  // underlying stream is drained.  We loop here because some chunks can
  // be StreamIdentifiers, padding, etc with no data.
  uint32_t bytesRead;
  do {
    nsresult rv = ParseNextChunk(&bytesRead);
    if (NS_WARN_IF(NS_FAILED(rv))) {
      return rv;
    }
    *aLengthOut = UncompressedLength();
  } while (*aLengthOut == 0 && bytesRead);

  return NS_OK;
}

NS_IMETHODIMP
SnappyUncompressInputStream::Read(char* aBuf, uint32_t aCount,
                                  uint32_t* aBytesReadOut) {
  return ReadSegments(NS_CopySegmentToBuffer, aBuf, aCount, aBytesReadOut);
}

NS_IMETHODIMP
SnappyUncompressInputStream::ReadSegments(nsWriteSegmentFun aWriter,
                                          void* aClosure, uint32_t aCount,
                                          uint32_t* aBytesReadOut) {
  *aBytesReadOut = 0;

  if (!mBaseStream) {
    return NS_BASE_STREAM_CLOSED;
  }

  nsresult rv;

  // Do not try to use the base stream's ReadSegements here.  Its very
  // unlikely we will get a single buffer that contains all of the compressed
  // data and therefore would have to copy into our own buffer anyways.
  // Instead, focus on making efficient use of the Read() interface.

  while (aCount > 0) {
    // We have some decompressed data in our buffer.  Provide it to the
    // callers writer function.
    if (mUncompressedBytes > 0) {
      MOZ_ASSERT(mUncompressedBuffer);
      uint32_t remaining = UncompressedLength();
      uint32_t numToWrite = std::min(aCount, remaining);
      uint32_t numWritten;
      rv = aWriter(this, aClosure, &mUncompressedBuffer[mNextByte],
                   *aBytesReadOut, numToWrite, &numWritten);

      // As defined in nsIInputputStream.idl, do not pass writer func errors.
      if (NS_FAILED(rv)) {
        return NS_OK;
      }

      // End-of-file
      if (numWritten == 0) {
        return NS_OK;
      }

      *aBytesReadOut += numWritten;
      mNextByte += numWritten;
      MOZ_ASSERT(mNextByte <= mUncompressedBytes);

      if (mNextByte == mUncompressedBytes) {
        mNextByte = 0;
        mUncompressedBytes = 0;
      }

      aCount -= numWritten;

      continue;
    }

    // Otherwise uncompress the next chunk and loop.  Any resulting data
    // will set mUncompressedBytes which we check at the top of the loop.
    uint32_t bytesRead;
    rv = ParseNextChunk(&bytesRead);
    if (NS_FAILED(rv)) {
      return rv;
    }

    // If we couldn't read anything and there is no more data to provide
    // to the caller, then this is eof.
    if (bytesRead == 0 && mUncompressedBytes == 0) {
      return NS_OK;
    }
  }

  return NS_OK;
}

NS_IMETHODIMP
SnappyUncompressInputStream::IsNonBlocking(bool* aNonBlockingOut) {
  *aNonBlockingOut = false;
  return NS_OK;
}

SnappyUncompressInputStream::~SnappyUncompressInputStream() { Close(); }

nsresult SnappyUncompressInputStream::ParseNextChunk(uint32_t* aBytesReadOut) {
  // There must not be any uncompressed data already in mUncompressedBuffer.
  MOZ_ASSERT(mUncompressedBytes == 0);
  MOZ_ASSERT(mNextByte == 0);

  nsresult rv;
  *aBytesReadOut = 0;

  // Lazily create our two buffers so we can report OOM during stream
  // operation.  These allocations only happens once.  The buffers are reused
  // until the stream is closed.
  if (!mUncompressedBuffer) {
    mUncompressedBuffer.reset(new (fallible) char[snappy::kBlockSize]);
    if (NS_WARN_IF(!mUncompressedBuffer)) {
      return NS_ERROR_OUT_OF_MEMORY;
    }
  }

  if (!mCompressedBuffer) {
    mCompressedBuffer.reset(new (fallible) char[CompressedBufferLength()]);
    if (NS_WARN_IF(!mCompressedBuffer)) {
      return NS_ERROR_OUT_OF_MEMORY;
    }
  }

  // We have no decompressed data and we also have not seen the start of stream
  // yet. Read and validate the StreamIdentifier chunk.  Also read the next
  // header to determine the size of the first real data chunk.
  if (mNeedFirstStreamIdentifier) {
    const uint32_t firstReadLength =
        kHeaderLength + kStreamIdentifierDataLength + kHeaderLength;
    MOZ_ASSERT(firstReadLength <= CompressedBufferLength());

    rv = ReadAll(mCompressedBuffer.get(), firstReadLength, firstReadLength,
                 aBytesReadOut);
    if (NS_WARN_IF(NS_FAILED(rv)) || *aBytesReadOut == 0) {
      return rv;
    }

    rv = ParseHeader(mCompressedBuffer.get(), kHeaderLength, &mNextChunkType,
                     &mNextChunkDataLength);
    if (NS_WARN_IF(NS_FAILED(rv))) {
      return rv;
    }
    if (NS_WARN_IF(mNextChunkType != StreamIdentifier ||
                   mNextChunkDataLength != kStreamIdentifierDataLength)) {
      return NS_ERROR_CORRUPTED_CONTENT;
    }
    size_t offset = kHeaderLength;

    mNeedFirstStreamIdentifier = false;

    size_t numRead;
    size_t numWritten;
    rv = ParseData(mUncompressedBuffer.get(), snappy::kBlockSize,
                   mNextChunkType, &mCompressedBuffer[offset],
                   mNextChunkDataLength, &numWritten, &numRead);
    if (NS_WARN_IF(NS_FAILED(rv))) {
      return rv;
    }
    MOZ_ASSERT(numWritten == 0);
    MOZ_ASSERT(numRead == mNextChunkDataLength);
    offset += numRead;

    rv = ParseHeader(&mCompressedBuffer[offset], *aBytesReadOut - offset,
                     &mNextChunkType, &mNextChunkDataLength);
    if (NS_WARN_IF(NS_FAILED(rv))) {
      return rv;
    }

    return NS_OK;
  }

  // We have no compressed data and we don't know how big the next chunk is.
  // This happens when we get an EOF pause in the middle of a stream and also
  // at the end of the stream.  Simply read the next header and return.  The
  // chunk body will be read on the next entry into this method.
  if (mNextChunkType == Unknown) {
    rv = ReadAll(mCompressedBuffer.get(), kHeaderLength, kHeaderLength,
                 aBytesReadOut);
    if (NS_WARN_IF(NS_FAILED(rv)) || *aBytesReadOut == 0) {
      return rv;
    }

    rv = ParseHeader(mCompressedBuffer.get(), kHeaderLength, &mNextChunkType,
                     &mNextChunkDataLength);
    if (NS_WARN_IF(NS_FAILED(rv))) {
      return rv;
    }

    return NS_OK;
  }

  // We have no decompressed data, but we do know the size of the next chunk.
  // Read at least that much from the base stream.
  uint32_t readLength = mNextChunkDataLength;
  MOZ_ASSERT(readLength <= CompressedBufferLength());

  // However, if there is enough data in the base stream, also read the next
  // chunk header.  This helps optimize the stream by avoiding many small reads.
  uint64_t avail;
  rv = mBaseStream->Available(&avail);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }
  if (avail >= (readLength + kHeaderLength)) {
    readLength += kHeaderLength;
    MOZ_ASSERT(readLength <= CompressedBufferLength());
  }

  rv = ReadAll(mCompressedBuffer.get(), readLength, mNextChunkDataLength,
               aBytesReadOut);
  if (NS_WARN_IF(NS_FAILED(rv)) || *aBytesReadOut == 0) {
    return rv;
  }

  size_t numRead;
  size_t numWritten;
  rv = ParseData(mUncompressedBuffer.get(), snappy::kBlockSize, mNextChunkType,
                 mCompressedBuffer.get(), mNextChunkDataLength, &numWritten,
                 &numRead);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }
  MOZ_ASSERT(numRead == mNextChunkDataLength);

  mUncompressedBytes = numWritten;

  // If we were unable to directly read the next chunk header, then clear
  // our internal state.  We will have to perform a small read to get the
  // header the next time we enter this method.
  if (*aBytesReadOut <= mNextChunkDataLength) {
    mNextChunkType = Unknown;
    mNextChunkDataLength = 0;
    return NS_OK;
  }

  // We got the next chunk header.  Parse it so that we are ready to for the
  // next call into this method.
  rv = ParseHeader(&mCompressedBuffer[numRead], *aBytesReadOut - numRead,
                   &mNextChunkType, &mNextChunkDataLength);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  return NS_OK;
}

nsresult SnappyUncompressInputStream::ReadAll(char* aBuf, uint32_t aCount,
                                              uint32_t aMinValidCount,
                                              uint32_t* aBytesReadOut) {
  MOZ_ASSERT(aCount >= aMinValidCount);

  *aBytesReadOut = 0;

  if (!mBaseStream) {
    return NS_BASE_STREAM_CLOSED;
  }

  uint32_t offset = 0;
  while (aCount > 0) {
    uint32_t bytesRead = 0;
    nsresult rv = mBaseStream->Read(aBuf + offset, aCount, &bytesRead);
    if (NS_WARN_IF(NS_FAILED(rv))) {
      return rv;
    }

    // EOF, but don't immediately return.  We need to validate min read bytes
    // below.
    if (bytesRead == 0) {
      break;
    }

    *aBytesReadOut += bytesRead;
    offset += bytesRead;
    aCount -= bytesRead;
  }

  // Reading zero bytes is not an error.  Its the expected EOF condition.
  // Only compare to the minimum valid count if we read at least one byte.
  if (*aBytesReadOut != 0 && *aBytesReadOut < aMinValidCount) {
    return NS_ERROR_CORRUPTED_CONTENT;
  }

  return NS_OK;
}

size_t SnappyUncompressInputStream::UncompressedLength() const {
  MOZ_ASSERT(mNextByte <= mUncompressedBytes);
  return mUncompressedBytes - mNextByte;
}

}  // namespace mozilla