Bug 1634436 - Add support for seeking (nsISeekableStream). r=dom-workers-and-storage-reviewers,janv

Differential Revision: https://phabricator.services.mozilla.com/D74669
This commit is contained in:
Simon Giesecke 2020-06-10 16:17:20 +00:00
Родитель de661a3fd7
Коммит 672ce1ce54
4 изменённых файлов: 426 добавлений и 5 удалений

Просмотреть файл

@ -9,11 +9,24 @@
namespace mozilla::dom::quota {
NS_IMPL_ISUPPORTS(DecryptingInputStreamBase, nsIInputStream);
NS_IMPL_ADDREF(DecryptingInputStreamBase);
NS_IMPL_RELEASE(DecryptingInputStreamBase);
NS_INTERFACE_MAP_BEGIN(DecryptingInputStreamBase)
NS_INTERFACE_MAP_ENTRY(nsIInputStream)
NS_INTERFACE_MAP_ENTRY(nsISeekableStream)
NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIInputStream)
NS_INTERFACE_MAP_END
DecryptingInputStreamBase::DecryptingInputStreamBase(
MovingNotNull<nsCOMPtr<nsIInputStream>> aBaseStream, size_t aBlockSize)
: mBaseStream(std::move(aBaseStream)), mBlockSize(aBlockSize) {}
: mBaseStream(std::move(aBaseStream)), mBlockSize(aBlockSize) {
const nsCOMPtr<nsISeekableStream> seekableStream =
do_QueryInterface(mBaseStream->get());
MOZ_ASSERT(seekableStream &&
SameCOMIdentity(mBaseStream->get(), seekableStream));
mBaseSeekableStream.init(WrapNotNullUnchecked(seekableStream));
}
NS_IMETHODIMP DecryptingInputStreamBase::Read(char* aBuf, uint32_t aCount,
uint32_t* aBytesReadOut) {
@ -25,6 +38,12 @@ NS_IMETHODIMP DecryptingInputStreamBase::IsNonBlocking(bool* aNonBlockingOut) {
return NS_OK;
}
NS_IMETHODIMP DecryptingInputStreamBase::SetEOF() {
// Cannot truncate a read-only stream.
return NS_ERROR_NOT_IMPLEMENTED;
}
size_t DecryptingInputStreamBase::PlainLength() const {
MOZ_ASSERT(mNextByte <= mPlainBytes);
return mPlainBytes - mNextByte;

Просмотреть файл

@ -10,18 +10,22 @@
#include "mozilla/InitializedOnce.h"
#include "nsCOMPtr.h"
#include "nsIInputStream.h"
#include "nsISeekableStream.h"
#include "EncryptedBlock.h"
namespace mozilla::dom::quota {
class DecryptingInputStreamBase : public nsIInputStream {
class DecryptingInputStreamBase : public nsIInputStream,
public nsISeekableStream {
public:
NS_DECL_THREADSAFE_ISUPPORTS
NS_IMETHOD Read(char* aBuf, uint32_t aCount, uint32_t* _retval) final;
NS_IMETHOD IsNonBlocking(bool* _retval) final;
NS_IMETHOD SetEOF() final;
protected:
DecryptingInputStreamBase(MovingNotNull<nsCOMPtr<nsIInputStream>> aBaseStream,
size_t aBlockSize);
@ -35,6 +39,7 @@ class DecryptingInputStreamBase : public nsIInputStream {
size_t EncryptedBufferLength() const;
InitializedOnce<const NotNull<nsCOMPtr<nsIInputStream>>> mBaseStream;
LazyInitializedOnce<const NotNull<nsISeekableStream*>> mBaseSeekableStream;
// Number of bytes of plain data in mBuffer.
size_t mPlainBytes = 0;
@ -43,6 +48,8 @@ class DecryptingInputStreamBase : public nsIInputStream {
size_t mNextByte = 0;
const size_t mBlockSize;
size_t mLastBlockLength = 0;
};
// Wraps another nsIInputStream which contains data written using
@ -63,6 +70,10 @@ class DecryptingInputStream final : public DecryptingInputStreamBase {
NS_IMETHOD ReadSegments(nsWriteSegmentFun aWriter, void* aClosure,
uint32_t aCount, uint32_t* _retval) override;
NS_DECL_NSITELLABLESTREAM
NS_IMETHOD Seek(int32_t aWhence, int64_t aOffset) override;
private:
~DecryptingInputStream();

Просмотреть файл

@ -126,6 +126,7 @@ NS_IMETHODIMP DecryptingInputStream<CipherStrategy>::ReadSegments(
if (mNextByte == mPlainBytes) {
mNextByte = 0;
mLastBlockLength = mPlainBytes;
mPlainBytes = 0;
}
@ -245,6 +246,167 @@ bool DecryptingInputStream<CipherStrategy>::EnsureBuffers() {
return true;
}
template <typename CipherStrategy>
NS_IMETHODIMP DecryptingInputStream<CipherStrategy>::Tell(
int64_t* const aRetval) {
MOZ_ASSERT(aRetval);
if (!mBaseStream) {
return NS_BASE_STREAM_CLOSED;
}
int64_t basePosition;
nsresult rv = (*mBaseSeekableStream)->Tell(&basePosition);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
const auto fullBlocks = basePosition / mBlockSize;
MOZ_ASSERT(0 == basePosition % mBlockSize);
*aRetval = (fullBlocks - ((mPlainBytes || mLastBlockLength) ? 1 : 0)) *
mEncryptedBlock->MaxPayloadLength() +
mNextByte + (mNextByte ? 0 : mLastBlockLength);
return NS_OK;
}
template <typename CipherStrategy>
NS_IMETHODIMP DecryptingInputStream<CipherStrategy>::Seek(const int32_t aWhence,
int64_t aOffset) {
if (!mBaseStream) {
return NS_BASE_STREAM_CLOSED;
}
if (!EnsureBuffers()) {
return NS_ERROR_OUT_OF_MEMORY;
}
int64_t baseBlocksOffset;
int64_t nextByteOffset;
switch (aWhence) {
case NS_SEEK_CUR:
// XXX Simplify this without using Tell.
{
int64_t current;
nsresult rv = Tell(&current);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
aOffset += current;
}
break;
case NS_SEEK_SET:
break;
case NS_SEEK_END:
// XXX Simplify this without using Seek/Tell.
{
// XXX The size of the stream could also be queried and stored once
// only.
nsresult rv = (*mBaseSeekableStream)->Seek(NS_SEEK_SET, 0);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
uint64_t baseStreamSize;
rv = (*mBaseStream)->Available(&baseStreamSize);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
auto decryptedStreamSizeOrErr = [baseStreamSize,
this]() -> Result<int64_t, nsresult> {
if (!baseStreamSize) {
return 0;
}
nsresult rv =
(*mBaseSeekableStream)
->Seek(NS_SEEK_END, -static_cast<int64_t>(mBlockSize));
if (NS_WARN_IF(NS_FAILED(rv))) {
return Err(rv);
}
uint32_t bytesRead;
rv = ParseNextChunk(&bytesRead);
if (NS_WARN_IF(NS_FAILED(rv))) {
return Err(rv);
}
MOZ_ASSERT(bytesRead);
// XXX Shouldn't ParseNextChunk better update mPlainBytes?
mPlainBytes = bytesRead;
mNextByte = bytesRead;
int64_t current;
rv = Tell(&current);
if (NS_WARN_IF(NS_FAILED(rv))) {
return Err(rv);
}
return current;
}();
if (decryptedStreamSizeOrErr.isErr()) {
return decryptedStreamSizeOrErr.unwrapErr();
}
aOffset += decryptedStreamSizeOrErr.unwrap();
}
break;
default:
return NS_ERROR_ILLEGAL_VALUE;
}
baseBlocksOffset = aOffset / mEncryptedBlock->MaxPayloadLength();
nextByteOffset = aOffset % mEncryptedBlock->MaxPayloadLength();
// XXX If we remain in the same block as before, we can skip this.
nsresult rv =
(*mBaseSeekableStream)->Seek(NS_SEEK_SET, baseBlocksOffset * mBlockSize);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
mNextByte = 0;
mPlainBytes = 0;
uint32_t readBytes;
rv = ParseNextChunk(&readBytes);
if (NS_WARN_IF(NS_FAILED(rv))) {
// XXX Do we need to do more here? Restore any previous state?
return rv;
}
// We positioned after the last block, we must read that to know its size.
// XXX We could know earlier if we positioned us after the last block.
if (!readBytes) {
if (baseBlocksOffset == 0) {
// The stream is empty.
return aOffset == 0 ? NS_OK : NS_ERROR_ILLEGAL_VALUE;
}
nsresult rv = (*mBaseSeekableStream)->Seek(NS_SEEK_CUR, -mBlockSize);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
rv = ParseNextChunk(&readBytes);
if (NS_WARN_IF(NS_FAILED(rv))) {
// XXX Do we need to do more here? Restore any previous state?
return rv;
}
}
mPlainBytes = readBytes;
mNextByte = nextByteOffset;
return NS_OK;
}
} // namespace mozilla::dom::quota
#endif

Просмотреть файл

@ -24,12 +24,14 @@ namespace mozilla::dom::quota {
// Similar to ArrayBufferInputStream from netwerk/base/ArrayBufferInputStream.h,
// but this is initialized from a Span on construction, rather than lazily from
// a JS ArrayBuffer.
class ArrayBufferInputStream : public nsIInputStream {
class ArrayBufferInputStream : public nsIInputStream, public nsISeekableStream {
public:
explicit ArrayBufferInputStream(mozilla::Span<const uint8_t> aData);
NS_DECL_THREADSAFE_ISUPPORTS
NS_DECL_NSIINPUTSTREAM
NS_DECL_NSITELLABLESTREAM
NS_DECL_NSISEEKABLESTREAM
private:
virtual ~ArrayBufferInputStream() = default;
@ -40,7 +42,14 @@ class ArrayBufferInputStream : public nsIInputStream {
bool mClosed;
};
NS_IMPL_ISUPPORTS(ArrayBufferInputStream, nsIInputStream);
NS_IMPL_ADDREF(ArrayBufferInputStream);
NS_IMPL_RELEASE(ArrayBufferInputStream);
NS_INTERFACE_MAP_BEGIN(ArrayBufferInputStream)
NS_INTERFACE_MAP_ENTRY(nsIInputStream)
NS_INTERFACE_MAP_ENTRY(nsISeekableStream)
NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIInputStream)
NS_INTERFACE_MAP_END
ArrayBufferInputStream::ArrayBufferInputStream(
mozilla::Span<const uint8_t> aData)
@ -127,6 +136,47 @@ ArrayBufferInputStream::IsNonBlocking(bool* aNonBlocking) {
return NS_OK;
}
NS_IMETHODIMP ArrayBufferInputStream::Tell(int64_t* const aRetval) {
MOZ_ASSERT(aRetval);
*aRetval = mPos;
return NS_OK;
}
NS_IMETHODIMP ArrayBufferInputStream::Seek(const int32_t aWhence,
const int64_t aOffset) {
// XXX This is not safe. it's hard to use CheckedInt here, though. As long as
// the class is only used for testing purposes, that's probably fine.
int32_t newPos = mPos;
switch (aWhence) {
case NS_SEEK_SET:
newPos = aOffset;
break;
case NS_SEEK_CUR:
newPos += aOffset;
break;
case NS_SEEK_END:
newPos = mBufferLength;
newPos += aOffset;
break;
default:
return NS_ERROR_ILLEGAL_VALUE;
}
if (newPos < 0 || static_cast<uint32_t>(newPos) > mBufferLength) {
return NS_ERROR_ILLEGAL_VALUE;
}
mPos = newPos;
return NS_OK;
}
NS_IMETHODIMP ArrayBufferInputStream::SetEOF() {
// Truncating is not supported on a read-only stream.
return NS_ERROR_NOT_IMPLEMENTED;
}
} // namespace mozilla::dom::quota
using namespace mozilla;
@ -273,6 +323,12 @@ static void ReadTestData(
EXPECT_EQ(currentExpected.Length(), read);
EXPECT_EQ(currentExpected,
Span{readData}.First(currentExpected.Length()).AsConst());
// Check that Tell tells the right position.
int64_t pos;
EXPECT_EQ(NS_OK, inStream->Tell(&pos));
EXPECT_EQ(aExpectedData.Length() - remainder.Length(),
static_cast<uint64_t>(pos));
}
// Expect EOF.
@ -383,6 +439,147 @@ TEST_P(ParametrizedCryptTest, DummyCipherStrategy_IncompleteBlock) {
readData.Length(), &read));
}
enum struct SeekOffset {
Zero,
MinusHalfDataSize,
PlusHalfDataSize,
PlusDataSize,
MinusDataSize
};
using SeekOp = std::pair<int32_t, SeekOffset>;
using PackedSeekTestParams = std::tuple<size_t, size_t, std::vector<SeekOp>>;
struct SeekTestParams {
size_t mDataSize;
size_t mBlockSize;
std::vector<SeekOp> mSeekOps;
MOZ_IMPLICIT SeekTestParams(const PackedSeekTestParams& aPackedParams)
: mDataSize(std::get<0>(aPackedParams)),
mBlockSize(std::get<1>(aPackedParams)),
mSeekOps(std::get<2>(aPackedParams)) {}
};
std::string SeekTestParamToString(
const testing::TestParamInfo<PackedSeekTestParams>& aTestParams) {
const SeekTestParams& testParams = aTestParams.param;
static constexpr char kSeparator[] = "_";
std::stringstream ss;
ss << "data" << testParams.mDataSize << kSeparator << "writechunk"
<< testParams.mBlockSize << kSeparator;
for (const auto& seekOp : testParams.mSeekOps) {
switch (seekOp.first) {
case nsISeekableStream::NS_SEEK_SET:
ss << "Set";
break;
case nsISeekableStream::NS_SEEK_CUR:
ss << "Cur";
break;
case nsISeekableStream::NS_SEEK_END:
ss << "End";
break;
};
switch (seekOp.second) {
case SeekOffset::Zero:
ss << "Zero";
break;
case SeekOffset::MinusHalfDataSize:
ss << "MinusHalfDataSize";
break;
case SeekOffset::PlusHalfDataSize:
ss << "PlusHalfDataSize";
break;
case SeekOffset::MinusDataSize:
ss << "MinusDataSize";
break;
case SeekOffset::PlusDataSize:
ss << "PlusDataSize";
break;
};
}
return ss.str();
}
class ParametrizedSeekCryptTest
: public DOM_Quota_EncryptedStream,
public testing::WithParamInterface<PackedSeekTestParams> {};
TEST_P(ParametrizedSeekCryptTest, DummyCipherStrategy_Seek) {
using CipherStrategy = DummyCipherStrategy;
const CipherStrategy cipherStrategy;
const SeekTestParams& testParams = GetParam();
const auto baseOutputStream =
WrapNotNull(RefPtr<dom::quota::MemoryOutputStream>{
dom::quota::MemoryOutputStream::Create(2048)});
const auto data = MakeTestData(testParams.mDataSize);
WriteTestData(nsCOMPtr<nsIOutputStream>{baseOutputStream.get()}, Span{data},
testParams.mDataSize, testParams.mBlockSize, cipherStrategy,
CipherStrategy::KeyType{}, FlushMode::Never);
const auto baseInputStream =
MakeRefPtr<ArrayBufferInputStream>(baseOutputStream->Data());
const auto inStream = MakeSafeRefPtr<DecryptingInputStream<CipherStrategy>>(
WrapNotNull(nsCOMPtr<nsIInputStream>{baseInputStream}),
testParams.mBlockSize, cipherStrategy, CipherStrategy::KeyType{});
uint32_t accumulatedOffset = 0;
for (const auto& seekOp : testParams.mSeekOps) {
const auto offset = [offsetKind = seekOp.second,
dataSize = testParams.mDataSize]() -> int64_t {
switch (offsetKind) {
case SeekOffset::Zero:
return 0;
case SeekOffset::MinusHalfDataSize:
return -static_cast<int64_t>(dataSize) / 2;
case SeekOffset::PlusHalfDataSize:
return dataSize / 2;
case SeekOffset::MinusDataSize:
return -static_cast<int64_t>(dataSize);
case SeekOffset::PlusDataSize:
return dataSize;
}
MOZ_CRASH("Unknown SeekOffset");
}();
switch (seekOp.first) {
case nsISeekableStream::NS_SEEK_SET:
accumulatedOffset = offset;
break;
case nsISeekableStream::NS_SEEK_CUR:
accumulatedOffset += offset;
break;
case nsISeekableStream::NS_SEEK_END:
accumulatedOffset = testParams.mDataSize + offset;
break;
}
EXPECT_EQ(NS_OK, inStream->Seek(seekOp.first, offset));
}
{
int64_t actualOffset;
EXPECT_EQ(NS_OK, inStream->Tell(&actualOffset));
EXPECT_EQ(actualOffset, accumulatedOffset);
}
auto readData = nsTArray<uint8_t>();
readData.SetLength(data.Length());
uint32_t read;
EXPECT_EQ(NS_OK, inStream->Read(reinterpret_cast<char*>(readData.Elements()),
readData.Length(), &read));
// XXX Or should 'read' indicate the actual number of bytes read,
// including the encryption overhead?
EXPECT_EQ(testParams.mDataSize - accumulatedOffset, read);
EXPECT_EQ(Span{data}.SplitAt(accumulatedOffset).second,
Span{readData}.First(read).AsConst());
}
INSTANTIATE_TEST_CASE_P(
DOM_Quota_EncryptedStream_Parametrized, ParametrizedCryptTest,
testing::Combine(
@ -397,3 +594,35 @@ INSTANTIATE_TEST_CASE_P(
/* flushMode */
testing::Values(FlushMode::Never, FlushMode::AfterEachChunk)),
TestParamToString);
INSTANTIATE_TEST_CASE_P(
DOM_IndexedDB_EncryptedStream_ParametrizedSeek, ParametrizedSeekCryptTest,
testing::Combine(
/* dataSize */ testing::Values(0u, 16u, 256u, 512u, 513u),
/* blockSize */ testing::Values(256u, 1024u /*, 8192u*/),
/* seekOperations */
testing::Values(/* NS_SEEK_SET only, single ops */
std::vector<SeekOp>{{nsISeekableStream::NS_SEEK_SET,
SeekOffset::PlusDataSize}},
std::vector<SeekOp>{{nsISeekableStream::NS_SEEK_SET,
SeekOffset::PlusHalfDataSize}},
/* NS_SEEK_SET only, multiple ops */
std::vector<SeekOp>{
{nsISeekableStream::NS_SEEK_SET,
SeekOffset::PlusHalfDataSize},
{nsISeekableStream::NS_SEEK_SET, SeekOffset::Zero}},
/* NS_SEEK_CUR only, single ops */
std::vector<SeekOp>{
{nsISeekableStream::NS_SEEK_CUR, SeekOffset::Zero}},
std::vector<SeekOp>{{nsISeekableStream::NS_SEEK_CUR,
SeekOffset::PlusDataSize}},
std::vector<SeekOp>{{nsISeekableStream::NS_SEEK_CUR,
SeekOffset::PlusHalfDataSize}},
/* NS_SEEK_END only, single ops */
std::vector<SeekOp>{
{nsISeekableStream::NS_SEEK_END, SeekOffset::Zero}},
std::vector<SeekOp>{{nsISeekableStream::NS_SEEK_END,
SeekOffset::MinusDataSize}},
std::vector<SeekOp>{{nsISeekableStream::NS_SEEK_END,
SeekOffset::MinusHalfDataSize}})),
SeekTestParamToString);