gecko-dev/image/SourceBuffer.cpp
Daniel Holbert 5041ff3d97 Bug 1273714: Remove an imagelib warning that's more spammy than useful at this point. r=tnikkel DONTBUILD
The condition that causes us to warn is typically a web-server configuration
issue, not a Firefox issue; and we handle it gracefully, so there's no real
point in adding to our debug-build terminal warning-spam.

Differential Revision: https://phabricator.services.mozilla.com/D189421
2023-09-27 23:27:13 +00:00

701 lines
22 KiB
C++

/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* 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 "SourceBuffer.h"
#include <algorithm>
#include <cmath>
#include <cstring>
#include "mozilla/Likely.h"
#include "nsIInputStream.h"
#include "MainThreadUtils.h"
#include "SurfaceCache.h"
using std::max;
using std::min;
namespace mozilla {
namespace image {
//////////////////////////////////////////////////////////////////////////////
// SourceBufferIterator implementation.
//////////////////////////////////////////////////////////////////////////////
SourceBufferIterator::~SourceBufferIterator() {
if (mOwner) {
mOwner->OnIteratorRelease();
}
}
SourceBufferIterator& SourceBufferIterator::operator=(
SourceBufferIterator&& aOther) {
if (mOwner) {
mOwner->OnIteratorRelease();
}
mOwner = std::move(aOther.mOwner);
mState = aOther.mState;
mData = aOther.mData;
mChunkCount = aOther.mChunkCount;
mByteCount = aOther.mByteCount;
mRemainderToRead = aOther.mRemainderToRead;
return *this;
}
SourceBufferIterator::State SourceBufferIterator::AdvanceOrScheduleResume(
size_t aRequestedBytes, IResumable* aConsumer) {
MOZ_ASSERT(mOwner);
if (MOZ_UNLIKELY(!HasMore())) {
MOZ_ASSERT_UNREACHABLE("Should not advance a completed iterator");
return COMPLETE;
}
// The range of data [mOffset, mOffset + mNextReadLength) has just been read
// by the caller (or at least they don't have any interest in it), so consume
// that data.
MOZ_ASSERT(mData.mIterating.mNextReadLength <=
mData.mIterating.mAvailableLength);
mData.mIterating.mOffset += mData.mIterating.mNextReadLength;
mData.mIterating.mAvailableLength -= mData.mIterating.mNextReadLength;
// An iterator can have a limit imposed on it to read only a subset of a
// source buffer. If it is present, we need to mimic the same behaviour as
// the owning SourceBuffer.
if (MOZ_UNLIKELY(mRemainderToRead != SIZE_MAX)) {
MOZ_ASSERT(mData.mIterating.mNextReadLength <= mRemainderToRead);
mRemainderToRead -= mData.mIterating.mNextReadLength;
if (MOZ_UNLIKELY(mRemainderToRead == 0)) {
mData.mIterating.mNextReadLength = 0;
SetComplete(NS_OK);
return COMPLETE;
}
if (MOZ_UNLIKELY(aRequestedBytes > mRemainderToRead)) {
aRequestedBytes = mRemainderToRead;
}
}
mData.mIterating.mNextReadLength = 0;
if (MOZ_LIKELY(mState == READY)) {
// If the caller wants zero bytes of data, that's easy enough; we just
// configured ourselves for a zero-byte read above! In theory we could do
// this even in the START state, but it's not important for performance and
// breaking the ability of callers to assert that the pointer returned by
// Data() is non-null doesn't seem worth it.
if (aRequestedBytes == 0) {
MOZ_ASSERT(mData.mIterating.mNextReadLength == 0);
return READY;
}
// Try to satisfy the request out of our local buffer. This is potentially
// much faster than requesting data from our owning SourceBuffer because we
// don't have to take the lock. Note that if we have anything at all in our
// local buffer, we use it to satisfy the request; @aRequestedBytes is just
// the *maximum* number of bytes we can return.
if (mData.mIterating.mAvailableLength > 0) {
return AdvanceFromLocalBuffer(aRequestedBytes);
}
}
// Our local buffer is empty, so we'll have to request data from our owning
// SourceBuffer.
return mOwner->AdvanceIteratorOrScheduleResume(*this, aRequestedBytes,
aConsumer);
}
bool SourceBufferIterator::RemainingBytesIsNoMoreThan(size_t aBytes) const {
MOZ_ASSERT(mOwner);
return mOwner->RemainingBytesIsNoMoreThan(*this, aBytes);
}
//////////////////////////////////////////////////////////////////////////////
// SourceBuffer implementation.
//////////////////////////////////////////////////////////////////////////////
const size_t SourceBuffer::MIN_CHUNK_CAPACITY;
const size_t SourceBuffer::MAX_CHUNK_CAPACITY;
SourceBuffer::SourceBuffer()
: mMutex("image::SourceBuffer"), mConsumerCount(0), mCompacted(false) {}
SourceBuffer::~SourceBuffer() {
MOZ_ASSERT(mConsumerCount == 0,
"SourceBuffer destroyed with active consumers");
}
nsresult SourceBuffer::AppendChunk(Maybe<Chunk>&& aChunk) {
mMutex.AssertCurrentThreadOwns();
if (MOZ_UNLIKELY(!aChunk)) {
return NS_ERROR_OUT_OF_MEMORY;
}
if (MOZ_UNLIKELY(aChunk->AllocationFailed())) {
return NS_ERROR_OUT_OF_MEMORY;
}
if (MOZ_UNLIKELY(!mChunks.AppendElement(std::move(*aChunk), fallible))) {
return NS_ERROR_OUT_OF_MEMORY;
}
return NS_OK;
}
Maybe<SourceBuffer::Chunk> SourceBuffer::CreateChunk(
size_t aCapacity, size_t aExistingCapacity /* = 0 */,
bool aRoundUp /* = true */) {
if (MOZ_UNLIKELY(aCapacity == 0)) {
MOZ_ASSERT_UNREACHABLE("Appending a chunk of zero size?");
return Nothing();
}
// Round up if requested.
size_t finalCapacity = aRoundUp ? RoundedUpCapacity(aCapacity) : aCapacity;
// Use the size of the SurfaceCache as an additional heuristic to avoid
// allocating huge buffers. Generally images do not get smaller when decoded,
// so if we could store the source data in the SurfaceCache, we assume that
// there's no way we'll be able to store the decoded version.
if (MOZ_UNLIKELY(!SurfaceCache::CanHold(finalCapacity + aExistingCapacity))) {
NS_WARNING(
"SourceBuffer refused to create chunk too large for SurfaceCache");
return Nothing();
}
return Some(Chunk(finalCapacity));
}
nsresult SourceBuffer::Compact() {
mMutex.AssertCurrentThreadOwns();
MOZ_ASSERT(mConsumerCount == 0, "Should have no consumers here");
MOZ_ASSERT(mWaitingConsumers.Length() == 0, "Shouldn't have waiters");
MOZ_ASSERT(mStatus, "Should be complete here");
// If we've tried to compact once, don't attempt again.
if (mCompacted) {
return NS_OK;
}
mCompacted = true;
// Compact our waiting consumers list, since we're complete and no future
// consumer will ever have to wait.
mWaitingConsumers.Compact();
// If we have no chunks, then there's nothing to compact.
if (mChunks.Length() < 1) {
return NS_OK;
}
// If we have one chunk, then we can compact if it has excess capacity.
if (mChunks.Length() == 1 && mChunks[0].Length() == mChunks[0].Capacity()) {
return NS_OK;
}
// If the last chunk has the maximum capacity, then we know the total size
// will be quite large and not worth consolidating. We can likely/cheapily
// trim the last chunk if it is too big however.
size_t capacity = mChunks.LastElement().Capacity();
if (capacity == MAX_CHUNK_CAPACITY) {
size_t lastLength = mChunks.LastElement().Length();
if (lastLength != capacity) {
mChunks.LastElement().SetCapacity(lastLength);
}
return NS_OK;
}
// We can compact our buffer. Determine the total length.
size_t length = 0;
for (uint32_t i = 0; i < mChunks.Length(); ++i) {
length += mChunks[i].Length();
}
// If our total length is zero (which means ExpectLength() got called, but no
// data ever actually got written) then just empty our chunk list.
if (MOZ_UNLIKELY(length == 0)) {
mChunks.Clear();
return NS_OK;
}
Chunk& mergeChunk = mChunks[0];
if (MOZ_UNLIKELY(!mergeChunk.SetCapacity(length))) {
NS_WARNING("Failed to reallocate chunk for SourceBuffer compacting - OOM?");
return NS_OK;
}
// Copy our old chunks into the newly reallocated first chunk.
for (uint32_t i = 1; i < mChunks.Length(); ++i) {
size_t offset = mergeChunk.Length();
MOZ_ASSERT(offset < mergeChunk.Capacity());
MOZ_ASSERT(offset + mChunks[i].Length() <= mergeChunk.Capacity());
memcpy(mergeChunk.Data() + offset, mChunks[i].Data(), mChunks[i].Length());
mergeChunk.AddLength(mChunks[i].Length());
}
MOZ_ASSERT(mergeChunk.Length() == mergeChunk.Capacity(),
"Compacted chunk has slack space");
// Remove the redundant chunks.
mChunks.RemoveLastElements(mChunks.Length() - 1);
mChunks.Compact();
return NS_OK;
}
/* static */
size_t SourceBuffer::RoundedUpCapacity(size_t aCapacity) {
// Protect against overflow.
if (MOZ_UNLIKELY(SIZE_MAX - aCapacity < MIN_CHUNK_CAPACITY)) {
return aCapacity;
}
// Round up to the next multiple of MIN_CHUNK_CAPACITY (which should be the
// size of a page).
size_t roundedCapacity =
(aCapacity + MIN_CHUNK_CAPACITY - 1) & ~(MIN_CHUNK_CAPACITY - 1);
MOZ_ASSERT(roundedCapacity >= aCapacity, "Bad math?");
MOZ_ASSERT(roundedCapacity - aCapacity < MIN_CHUNK_CAPACITY, "Bad math?");
return roundedCapacity;
}
size_t SourceBuffer::FibonacciCapacityWithMinimum(size_t aMinCapacity) {
mMutex.AssertCurrentThreadOwns();
// We grow the source buffer using a Fibonacci growth rate. It will be capped
// at MAX_CHUNK_CAPACITY, unless the available data exceeds that.
size_t length = mChunks.Length();
if (length == 0 || aMinCapacity > MAX_CHUNK_CAPACITY) {
return aMinCapacity;
}
if (length == 1) {
return min(max(2 * mChunks[0].Capacity(), aMinCapacity),
MAX_CHUNK_CAPACITY);
}
return min(
max(mChunks[length - 1].Capacity() + mChunks[length - 2].Capacity(),
aMinCapacity),
MAX_CHUNK_CAPACITY);
}
void SourceBuffer::AddWaitingConsumer(IResumable* aConsumer) {
mMutex.AssertCurrentThreadOwns();
MOZ_ASSERT(!mStatus, "Waiting when we're complete?");
if (aConsumer) {
mWaitingConsumers.AppendElement(aConsumer);
}
}
void SourceBuffer::ResumeWaitingConsumers() {
mMutex.AssertCurrentThreadOwns();
if (mWaitingConsumers.Length() == 0) {
return;
}
for (uint32_t i = 0; i < mWaitingConsumers.Length(); ++i) {
mWaitingConsumers[i]->Resume();
}
mWaitingConsumers.Clear();
}
nsresult SourceBuffer::ExpectLength(size_t aExpectedLength) {
MOZ_ASSERT(aExpectedLength > 0, "Zero expected size?");
MutexAutoLock lock(mMutex);
if (MOZ_UNLIKELY(mStatus)) {
MOZ_ASSERT_UNREACHABLE("ExpectLength after SourceBuffer is complete");
return NS_OK;
}
if (MOZ_UNLIKELY(mChunks.Length() > 0)) {
MOZ_ASSERT_UNREACHABLE("Duplicate or post-Append call to ExpectLength");
return NS_OK;
}
if (MOZ_UNLIKELY(!SurfaceCache::CanHold(aExpectedLength))) {
NS_WARNING("SourceBuffer refused to store too large buffer");
return HandleError(NS_ERROR_INVALID_ARG);
}
size_t length = min(aExpectedLength, MAX_CHUNK_CAPACITY);
if (MOZ_UNLIKELY(NS_FAILED(AppendChunk(CreateChunk(length,
/* aExistingCapacity */ 0,
/* aRoundUp */ false))))) {
return HandleError(NS_ERROR_OUT_OF_MEMORY);
}
return NS_OK;
}
nsresult SourceBuffer::Append(const char* aData, size_t aLength) {
MOZ_ASSERT(aData, "Should have a buffer");
MOZ_ASSERT(aLength > 0, "Writing a zero-sized chunk");
size_t currentChunkCapacity = 0;
size_t currentChunkLength = 0;
char* currentChunkData = nullptr;
size_t currentChunkRemaining = 0;
size_t forCurrentChunk = 0;
size_t forNextChunk = 0;
size_t nextChunkCapacity = 0;
size_t totalCapacity = 0;
{
MutexAutoLock lock(mMutex);
if (MOZ_UNLIKELY(mStatus)) {
// This SourceBuffer is already complete; ignore further data.
return NS_ERROR_FAILURE;
}
if (MOZ_UNLIKELY(mChunks.Length() == 0)) {
if (MOZ_UNLIKELY(NS_FAILED(AppendChunk(CreateChunk(aLength))))) {
return HandleError(NS_ERROR_OUT_OF_MEMORY);
}
}
// Copy out the current chunk's information so we can release the lock.
// Note that this wouldn't be safe if multiple producers were allowed!
Chunk& currentChunk = mChunks.LastElement();
currentChunkCapacity = currentChunk.Capacity();
currentChunkLength = currentChunk.Length();
currentChunkData = currentChunk.Data();
// Partition this data between the current chunk and the next chunk.
// (Because we always allocate a chunk big enough to fit everything passed
// to Append, we'll never need more than those two chunks to store
// everything.)
currentChunkRemaining = currentChunkCapacity - currentChunkLength;
forCurrentChunk = min(aLength, currentChunkRemaining);
forNextChunk = aLength - forCurrentChunk;
// If we'll need another chunk, determine what its capacity should be while
// we still hold the lock.
nextChunkCapacity =
forNextChunk > 0 ? FibonacciCapacityWithMinimum(forNextChunk) : 0;
for (uint32_t i = 0; i < mChunks.Length(); ++i) {
totalCapacity += mChunks[i].Capacity();
}
}
// Write everything we can fit into the current chunk.
MOZ_ASSERT(currentChunkLength + forCurrentChunk <= currentChunkCapacity);
memcpy(currentChunkData + currentChunkLength, aData, forCurrentChunk);
// If there's something left, create a new chunk and write it there.
Maybe<Chunk> nextChunk;
if (forNextChunk > 0) {
MOZ_ASSERT(nextChunkCapacity >= forNextChunk, "Next chunk too small?");
nextChunk = CreateChunk(nextChunkCapacity, totalCapacity);
if (MOZ_LIKELY(nextChunk && !nextChunk->AllocationFailed())) {
memcpy(nextChunk->Data(), aData + forCurrentChunk, forNextChunk);
nextChunk->AddLength(forNextChunk);
}
}
// Update shared data structures.
{
MutexAutoLock lock(mMutex);
// Update the length of the current chunk.
Chunk& currentChunk = mChunks.LastElement();
MOZ_ASSERT(currentChunk.Data() == currentChunkData, "Multiple producers?");
MOZ_ASSERT(currentChunk.Length() == currentChunkLength,
"Multiple producers?");
currentChunk.AddLength(forCurrentChunk);
// If we created a new chunk, add it to the series.
if (forNextChunk > 0) {
if (MOZ_UNLIKELY(!nextChunk)) {
return HandleError(NS_ERROR_OUT_OF_MEMORY);
}
if (MOZ_UNLIKELY(NS_FAILED(AppendChunk(std::move(nextChunk))))) {
return HandleError(NS_ERROR_OUT_OF_MEMORY);
}
}
// Resume any waiting readers now that there's new data.
ResumeWaitingConsumers();
}
return NS_OK;
}
static nsresult AppendToSourceBuffer(nsIInputStream*, void* aClosure,
const char* aFromRawSegment, uint32_t,
uint32_t aCount, uint32_t* aWriteCount) {
SourceBuffer* sourceBuffer = static_cast<SourceBuffer*>(aClosure);
// Copy the source data. Unless we hit OOM, we squelch the return value here,
// because returning an error means that ReadSegments stops reading data, and
// we want to ensure that we read everything we get. If we hit OOM then we
// return a failed status to the caller.
nsresult rv = sourceBuffer->Append(aFromRawSegment, aCount);
if (rv == NS_ERROR_OUT_OF_MEMORY) {
return rv;
}
// Report that we wrote everything we got.
*aWriteCount = aCount;
return NS_OK;
}
nsresult SourceBuffer::AppendFromInputStream(nsIInputStream* aInputStream,
uint32_t aCount) {
uint32_t bytesRead;
nsresult rv = aInputStream->ReadSegments(AppendToSourceBuffer, this, aCount,
&bytesRead);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
if (bytesRead == 0) {
// The loading of the image has been canceled.
return NS_ERROR_FAILURE;
}
if (bytesRead != aCount) {
// Only some of the given data was read. We may have failed in
// SourceBuffer::Append but ReadSegments swallowed the error. Otherwise the
// stream itself failed to yield the data.
MutexAutoLock lock(mMutex);
if (mStatus) {
MOZ_ASSERT(NS_FAILED(*mStatus));
return *mStatus;
}
MOZ_ASSERT_UNREACHABLE("AppendToSourceBuffer should consume everything");
}
return rv;
}
void SourceBuffer::Complete(nsresult aStatus) {
MutexAutoLock lock(mMutex);
// When an error occurs internally (e.g. due to an OOM), we save the status.
// This will indirectly trigger a failure higher up and that will call
// SourceBuffer::Complete. Since it doesn't necessarily know we are already
// complete, it is safe to ignore.
if (mStatus && (MOZ_UNLIKELY(NS_SUCCEEDED(*mStatus) ||
aStatus != NS_IMAGELIB_ERROR_FAILURE))) {
MOZ_ASSERT_UNREACHABLE("Called Complete more than once");
return;
}
if (MOZ_UNLIKELY(NS_SUCCEEDED(aStatus) && IsEmpty())) {
// It's illegal to succeed without writing anything.
aStatus = NS_ERROR_FAILURE;
}
mStatus = Some(aStatus);
// Resume any waiting consumers now that we're complete.
ResumeWaitingConsumers();
// If we still have active consumers, just return.
if (mConsumerCount > 0) {
return;
}
// Attempt to compact our buffer down to a single chunk.
Compact();
}
bool SourceBuffer::IsComplete() {
MutexAutoLock lock(mMutex);
return bool(mStatus);
}
size_t SourceBuffer::SizeOfIncludingThisWithComputedFallback(
MallocSizeOf aMallocSizeOf) const {
MutexAutoLock lock(mMutex);
size_t n = aMallocSizeOf(this);
n += mChunks.ShallowSizeOfExcludingThis(aMallocSizeOf);
for (uint32_t i = 0; i < mChunks.Length(); ++i) {
size_t chunkSize = aMallocSizeOf(mChunks[i].Data());
if (chunkSize == 0) {
// We're on a platform where moz_malloc_size_of always returns 0.
chunkSize = mChunks[i].Capacity();
}
n += chunkSize;
}
return n;
}
SourceBufferIterator SourceBuffer::Iterator(size_t aReadLength) {
{
MutexAutoLock lock(mMutex);
mConsumerCount++;
}
return SourceBufferIterator(this, aReadLength);
}
void SourceBuffer::OnIteratorRelease() {
MutexAutoLock lock(mMutex);
MOZ_ASSERT(mConsumerCount > 0, "Consumer count doesn't add up");
mConsumerCount--;
// If we still have active consumers, or we're not complete yet, then return.
if (mConsumerCount > 0 || !mStatus) {
return;
}
// Attempt to compact our buffer down to a single chunk.
Compact();
}
bool SourceBuffer::RemainingBytesIsNoMoreThan(
const SourceBufferIterator& aIterator, size_t aBytes) const {
MutexAutoLock lock(mMutex);
// If we're not complete, we always say no.
if (!mStatus) {
return false;
}
// If the iterator's at the end, the answer is trivial.
if (!aIterator.HasMore()) {
return true;
}
uint32_t iteratorChunk = aIterator.mData.mIterating.mChunk;
size_t iteratorOffset = aIterator.mData.mIterating.mOffset;
size_t iteratorLength = aIterator.mData.mIterating.mAvailableLength;
// Include the bytes the iterator is currently pointing to in the limit, so
// that the current chunk doesn't have to be a special case.
size_t bytes = aBytes + iteratorOffset + iteratorLength;
// Count the length over all of our chunks, starting with the one that the
// iterator is currently pointing to. (This is O(N), but N is expected to be
// ~1, so it doesn't seem worth caching the length separately.)
size_t lengthSoFar = 0;
for (uint32_t i = iteratorChunk; i < mChunks.Length(); ++i) {
lengthSoFar += mChunks[i].Length();
if (lengthSoFar > bytes) {
return false;
}
}
return true;
}
SourceBufferIterator::State SourceBuffer::AdvanceIteratorOrScheduleResume(
SourceBufferIterator& aIterator, size_t aRequestedBytes,
IResumable* aConsumer) {
MutexAutoLock lock(mMutex);
MOZ_ASSERT(aIterator.HasMore(),
"Advancing a completed iterator and "
"AdvanceOrScheduleResume didn't catch it");
if (MOZ_UNLIKELY(mStatus && NS_FAILED(*mStatus))) {
// This SourceBuffer is complete due to an error; all reads fail.
return aIterator.SetComplete(*mStatus);
}
if (MOZ_UNLIKELY(mChunks.Length() == 0)) {
// We haven't gotten an initial chunk yet.
AddWaitingConsumer(aConsumer);
return aIterator.SetWaiting(!!aConsumer);
}
uint32_t iteratorChunkIdx = aIterator.mData.mIterating.mChunk;
MOZ_ASSERT(iteratorChunkIdx < mChunks.Length());
const Chunk& currentChunk = mChunks[iteratorChunkIdx];
size_t iteratorEnd = aIterator.mData.mIterating.mOffset +
aIterator.mData.mIterating.mAvailableLength;
MOZ_ASSERT(iteratorEnd <= currentChunk.Length());
MOZ_ASSERT(iteratorEnd <= currentChunk.Capacity());
if (iteratorEnd < currentChunk.Length()) {
// There's more data in the current chunk.
return aIterator.SetReady(iteratorChunkIdx, currentChunk.Data(),
iteratorEnd, currentChunk.Length() - iteratorEnd,
aRequestedBytes);
}
if (iteratorEnd == currentChunk.Capacity() &&
!IsLastChunk(iteratorChunkIdx)) {
// Advance to the next chunk.
const Chunk& nextChunk = mChunks[iteratorChunkIdx + 1];
return aIterator.SetReady(iteratorChunkIdx + 1, nextChunk.Data(), 0,
nextChunk.Length(), aRequestedBytes);
}
MOZ_ASSERT(IsLastChunk(iteratorChunkIdx), "Should've advanced");
if (mStatus) {
// There's no more data and this SourceBuffer completed successfully.
MOZ_ASSERT(NS_SUCCEEDED(*mStatus), "Handled failures earlier");
return aIterator.SetComplete(*mStatus);
}
// We're not complete, but there's no more data right now. Arrange to wake up
// the consumer when we get more data.
AddWaitingConsumer(aConsumer);
return aIterator.SetWaiting(!!aConsumer);
}
nsresult SourceBuffer::HandleError(nsresult aError) {
MOZ_ASSERT(NS_FAILED(aError), "Should have an error here");
MOZ_ASSERT(aError == NS_ERROR_OUT_OF_MEMORY || aError == NS_ERROR_INVALID_ARG,
"Unexpected error; may want to notify waiting readers, which "
"HandleError currently doesn't do");
mMutex.AssertCurrentThreadOwns();
NS_WARNING("SourceBuffer encountered an unrecoverable error");
// Record the error.
mStatus = Some(aError);
// Drop our references to waiting readers.
mWaitingConsumers.Clear();
return *mStatus;
}
bool SourceBuffer::IsEmpty() {
mMutex.AssertCurrentThreadOwns();
return mChunks.Length() == 0 || mChunks[0].Length() == 0;
}
bool SourceBuffer::IsLastChunk(uint32_t aChunk) {
mMutex.AssertCurrentThreadOwns();
return aChunk + 1 == mChunks.Length();
}
} // namespace image
} // namespace mozilla