/* -*- 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 "gtest/gtest.h" #include #include #include #include #include #include #include #include #include #include #include "ErrorList.h" #include "mozilla/AlreadyAddRefed.h" #include "mozilla/Assertions.h" #include "mozilla/Attributes.h" #include "mozilla/FixedBufferOutputStream.h" #include "mozilla/NotNull.h" #include "mozilla/RefPtr.h" #include "mozilla/Scoped.h" #include "mozilla/Span.h" #include "mozilla/UniquePtr.h" #include "mozilla/dom/SafeRefPtr.h" #include "mozilla/dom/quota/DecryptingInputStream_impl.h" #include "mozilla/dom/quota/DummyCipherStrategy.h" #include "mozilla/dom/quota/EncryptedBlock.h" #include "mozilla/dom/quota/EncryptingOutputStream_impl.h" #include "mozilla/dom/quota/NSSCipherStrategy.h" #include "mozilla/fallible.h" #include "nsCOMPtr.h" #include "nsError.h" #include "nsICloneableInputStream.h" #include "nsIInputStream.h" #include "nsIOutputStream.h" #include "nsISeekableStream.h" #include "nsISupports.h" #include "nsITellableStream.h" #include "nsStreamUtils.h" #include "nsString.h" #include "nsStringFwd.h" #include "nsTArray.h" #include "nscore.h" #include "nss.h" 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, public nsISeekableStream, public nsICloneableInputStream { public: explicit ArrayBufferInputStream(mozilla::Span aData); NS_DECL_THREADSAFE_ISUPPORTS NS_DECL_NSIINPUTSTREAM NS_DECL_NSITELLABLESTREAM NS_DECL_NSISEEKABLESTREAM NS_DECL_NSICLONEABLEINPUTSTREAM private: virtual ~ArrayBufferInputStream() = default; mozilla::UniquePtr mArrayBuffer; uint32_t mBufferLength; uint32_t mPos; bool mClosed; }; 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(nsICloneableInputStream) NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIInputStream) NS_INTERFACE_MAP_END ArrayBufferInputStream::ArrayBufferInputStream( mozilla::Span aData) : mArrayBuffer(MakeUnique(aData.Length())), mBufferLength(aData.Length()), mPos(0), mClosed(false) { std::copy(aData.cbegin(), aData.cend(), mArrayBuffer.get()); } NS_IMETHODIMP ArrayBufferInputStream::Close() { mClosed = true; return NS_OK; } NS_IMETHODIMP ArrayBufferInputStream::Available(uint64_t* aCount) { if (mClosed) { return NS_BASE_STREAM_CLOSED; } if (mArrayBuffer) { *aCount = mBufferLength ? mBufferLength - mPos : 0; } else { *aCount = 0; } return NS_OK; } NS_IMETHODIMP ArrayBufferInputStream::StreamStatus() { return mClosed ? NS_BASE_STREAM_CLOSED : NS_OK; } NS_IMETHODIMP ArrayBufferInputStream::Read(char* aBuf, uint32_t aCount, uint32_t* aReadCount) { return ReadSegments(NS_CopySegmentToBuffer, aBuf, aCount, aReadCount); } NS_IMETHODIMP ArrayBufferInputStream::ReadSegments(nsWriteSegmentFun writer, void* closure, uint32_t aCount, uint32_t* result) { MOZ_ASSERT(result, "null ptr"); MOZ_ASSERT(mBufferLength >= mPos, "bad stream state"); if (mClosed) { return NS_BASE_STREAM_CLOSED; } MOZ_ASSERT(mArrayBuffer || (mPos == mBufferLength), "stream inited incorrectly"); *result = 0; while (mPos < mBufferLength) { uint32_t remaining = mBufferLength - mPos; MOZ_ASSERT(mArrayBuffer); uint32_t count = std::min(aCount, remaining); if (count == 0) { break; } uint32_t written; nsresult rv = writer(this, closure, &mArrayBuffer[0] + mPos, *result, count, &written); if (NS_FAILED(rv)) { // InputStreams do not propagate errors to caller. return NS_OK; } MOZ_ASSERT(written <= count, "writer should not write more than we asked it to write"); mPos += written; *result += written; aCount -= written; } return NS_OK; } NS_IMETHODIMP ArrayBufferInputStream::IsNonBlocking(bool* aNonBlocking) { // Actually, the stream never blocks, but we lie about it because of the // assumptions in DecryptingInputStream. *aNonBlocking = false; 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(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; } NS_IMETHODIMP ArrayBufferInputStream::GetCloneable(bool* aCloneable) { *aCloneable = true; return NS_OK; } NS_IMETHODIMP ArrayBufferInputStream::Clone(nsIInputStream** _retval) { *_retval = MakeAndAddRef( AsBytes(Span{mArrayBuffer.get(), mBufferLength})) .take(); return NS_OK; } } // namespace mozilla::dom::quota namespace mozilla { MOZ_TYPE_SPECIFIC_SCOPED_POINTER_TEMPLATE(ScopedNSSContext, NSSInitContext, NSS_ShutdownContext); } // namespace mozilla using namespace mozilla; using namespace mozilla::dom::quota; class DOM_Quota_EncryptedStream : public ::testing::Test { public: static void SetUpTestCase() { // Do this only once, do not tear it down per test case. if (!sNssContext) { sNssContext = NSS_InitContext("", "", "", "", nullptr, NSS_INIT_READONLY | NSS_INIT_NOCERTDB | NSS_INIT_NOMODDB | NSS_INIT_FORCEOPEN | NSS_INIT_OPTIMIZESPACE | NSS_INIT_NOROOTINIT); } } static void TearDownTestCase() { sNssContext = nullptr; } private: inline static ScopedNSSContext sNssContext = ScopedNSSContext{}; }; enum struct FlushMode { AfterEachChunk, Never }; enum struct ChunkSize { SingleByte, Unaligned, DataSize }; using PackedTestParams = std::tuple; static size_t EffectiveChunkSize(const ChunkSize aChunkSize, const size_t aDataSize) { switch (aChunkSize) { case ChunkSize::SingleByte: return 1; case ChunkSize::Unaligned: return 17; case ChunkSize::DataSize: return aDataSize; } MOZ_CRASH("Unknown ChunkSize"); } struct TestParams { MOZ_IMPLICIT constexpr TestParams(const PackedTestParams& aPackedParams) : mDataSize(std::get<0>(aPackedParams)), mWriteChunkSize(std::get<1>(aPackedParams)), mReadChunkSize(std::get<2>(aPackedParams)), mBlockSize(std::get<3>(aPackedParams)), mFlushMode(std::get<4>(aPackedParams)) {} constexpr size_t DataSize() const { return mDataSize; } size_t EffectiveWriteChunkSize() const { return EffectiveChunkSize(mWriteChunkSize, mDataSize); } size_t EffectiveReadChunkSize() const { return EffectiveChunkSize(mReadChunkSize, mDataSize); } constexpr size_t BlockSize() const { return mBlockSize; } constexpr enum FlushMode FlushMode() const { return mFlushMode; } private: size_t mDataSize; ChunkSize mWriteChunkSize; ChunkSize mReadChunkSize; size_t mBlockSize; enum FlushMode mFlushMode; }; std::string TestParamToString( const testing::TestParamInfo& aTestParams) { const TestParams& testParams = aTestParams.param; static constexpr char kSeparator[] = "_"; std::stringstream ss; ss << "data" << testParams.DataSize() << kSeparator << "writechunk" << testParams.EffectiveWriteChunkSize() << kSeparator << "readchunk" << testParams.EffectiveReadChunkSize() << kSeparator << "block" << testParams.BlockSize() << kSeparator; switch (testParams.FlushMode()) { case FlushMode::Never: ss << "FlushNever"; break; case FlushMode::AfterEachChunk: ss << "FlushAfterEachChunk"; break; }; return ss.str(); } class ParametrizedCryptTest : public DOM_Quota_EncryptedStream, public testing::WithParamInterface {}; static auto MakeTestData(const size_t aDataSize) { auto data = nsTArray(); data.SetLength(aDataSize); std::iota(data.begin(), data.end(), 0); return data; } template static void WriteTestData(nsCOMPtr&& aBaseOutputStream, const Span aData, const size_t aWriteChunkSize, const size_t aBlockSize, const typename CipherStrategy::KeyType& aKey, const FlushMode aFlushMode) { auto outStream = MakeSafeRefPtr>( std::move(aBaseOutputStream), aBlockSize, aKey); for (auto remaining = aData; !remaining.IsEmpty();) { auto [currentChunk, newRemaining] = remaining.SplitAt(std::min(aWriteChunkSize, remaining.Length())); remaining = newRemaining; uint32_t written; EXPECT_EQ(NS_OK, outStream->Write( reinterpret_cast(currentChunk.Elements()), currentChunk.Length(), &written)); EXPECT_EQ(currentChunk.Length(), written); if (aFlushMode == FlushMode::AfterEachChunk) { outStream->Flush(); } } // Close explicitly so we can check the result. EXPECT_EQ(NS_OK, outStream->Close()); } template static void NoExtraChecks(DecryptingInputStream& aInputStream, Span aExpectedData, Span aRemainder) {} template )> static void ReadTestData( DecryptingInputStream& aDecryptingInputStream, const Span aExpectedData, const size_t aReadChunkSize, const ExtraChecks& aExtraChecks = NoExtraChecks) { auto readData = nsTArray(); readData.SetLength(aReadChunkSize); for (auto remainder = aExpectedData; !remainder.IsEmpty();) { auto [currentExpected, newExpectedRemainder] = remainder.SplitAt(std::min(aReadChunkSize, remainder.Length())); remainder = newExpectedRemainder; uint32_t read; EXPECT_EQ(NS_OK, aDecryptingInputStream.Read( reinterpret_cast(readData.Elements()), currentExpected.Length(), &read)); EXPECT_EQ(currentExpected.Length(), read); EXPECT_EQ(currentExpected, Span{readData}.First(currentExpected.Length()).AsConst()); aExtraChecks(aDecryptingInputStream, aExpectedData, remainder); } // Expect EOF. uint32_t read; EXPECT_EQ(NS_OK, aDecryptingInputStream.Read( reinterpret_cast(readData.Elements()), readData.Length(), &read)); EXPECT_EQ(0u, read); } template )> static auto ReadTestData( MovingNotNull>&& aBaseInputStream, const Span aExpectedData, const size_t aReadChunkSize, const size_t aBlockSize, const typename CipherStrategy::KeyType& aKey, const ExtraChecks& aExtraChecks = NoExtraChecks) { auto inStream = MakeSafeRefPtr>( std::move(aBaseInputStream), aBlockSize, aKey); ReadTestData(*inStream, aExpectedData, aReadChunkSize, aExtraChecks); return inStream; } // XXX Change to return the buffer instead. template )> static RefPtr DoRoundtripTest( const size_t aDataSize, const size_t aWriteChunkSize, const size_t aReadChunkSize, const size_t aBlockSize, const typename CipherStrategy::KeyType& aKey, const FlushMode aFlushMode, const ExtraChecks& aExtraChecks = NoExtraChecks) { // XXX Add deduction guide for RefPtr from already_AddRefed const auto baseOutputStream = WrapNotNull( RefPtr{FixedBufferOutputStream::Create(2048)}); const auto data = MakeTestData(aDataSize); WriteTestData( nsCOMPtr{baseOutputStream.get()}, Span{data}, aWriteChunkSize, aBlockSize, aKey, aFlushMode); const auto baseInputStream = MakeRefPtr(baseOutputStream->WrittenData()); ReadTestData( WrapNotNull(nsCOMPtr{baseInputStream}), Span{data}, aReadChunkSize, aBlockSize, aKey, aExtraChecks); return baseOutputStream; } TEST_P(ParametrizedCryptTest, NSSCipherStrategy) { using CipherStrategy = NSSCipherStrategy; const TestParams& testParams = GetParam(); auto keyOrErr = CipherStrategy::GenerateKey(); ASSERT_FALSE(keyOrErr.isErr()); DoRoundtripTest( testParams.DataSize(), testParams.EffectiveWriteChunkSize(), testParams.EffectiveReadChunkSize(), testParams.BlockSize(), keyOrErr.unwrap(), testParams.FlushMode()); } TEST_P(ParametrizedCryptTest, DummyCipherStrategy_CheckOutput) { using CipherStrategy = DummyCipherStrategy; const TestParams& testParams = GetParam(); const auto encryptedDataStream = DoRoundtripTest( testParams.DataSize(), testParams.EffectiveWriteChunkSize(), testParams.EffectiveReadChunkSize(), testParams.BlockSize(), CipherStrategy::KeyType{}, testParams.FlushMode()); if (HasFailure()) { return; } const auto encryptedData = encryptedDataStream->WrittenData(); const auto encryptedDataSpan = AsBytes(Span(encryptedData)); const auto plainTestData = MakeTestData(testParams.DataSize()); auto encryptedBlock = EncryptedBlock{ testParams.BlockSize(), }; for (auto [encryptedRemainder, plainRemainder] = std::pair(encryptedDataSpan, Span(plainTestData)); !encryptedRemainder.IsEmpty();) { const auto [currentBlock, newEncryptedRemainder] = encryptedRemainder.SplitAt(testParams.BlockSize()); encryptedRemainder = newEncryptedRemainder; std::copy(currentBlock.cbegin(), currentBlock.cend(), encryptedBlock.MutableWholeBlock().begin()); ASSERT_FALSE(plainRemainder.IsEmpty()); const auto [currentPlain, newPlainRemainder] = plainRemainder.SplitAt(encryptedBlock.ActualPayloadLength()); plainRemainder = newPlainRemainder; const auto pseudoIV = encryptedBlock.CipherPrefix(); const auto payload = encryptedBlock.Payload(); EXPECT_EQ(Span(DummyCipherStrategy::MakeBlockPrefix()), pseudoIV); auto untransformedPayload = nsTArray(); untransformedPayload.SetLength(testParams.BlockSize()); DummyCipherStrategy::DummyTransform(payload, untransformedPayload); EXPECT_EQ( currentPlain, Span(untransformedPayload).AsConst().First(currentPlain.Length())); } } TEST_P(ParametrizedCryptTest, DummyCipherStrategy_Tell) { using CipherStrategy = DummyCipherStrategy; const TestParams& testParams = GetParam(); DoRoundtripTest( testParams.DataSize(), testParams.EffectiveWriteChunkSize(), testParams.EffectiveReadChunkSize(), testParams.BlockSize(), CipherStrategy::KeyType{}, testParams.FlushMode(), [](auto& inStream, Span expectedData, Span remainder) { // Check that Tell tells the right position. int64_t pos; EXPECT_EQ(NS_OK, inStream.Tell(&pos)); EXPECT_EQ(expectedData.Length() - remainder.Length(), static_cast(pos)); }); } TEST_P(ParametrizedCryptTest, DummyCipherStrategy_Available) { using CipherStrategy = DummyCipherStrategy; const TestParams& testParams = GetParam(); DoRoundtripTest( testParams.DataSize(), testParams.EffectiveWriteChunkSize(), testParams.EffectiveReadChunkSize(), testParams.BlockSize(), CipherStrategy::KeyType{}, testParams.FlushMode(), [](auto& inStream, Span expectedData, Span remainder) { // Check that Available tells the right remainder. uint64_t available; EXPECT_EQ(NS_OK, inStream.Available(&available)); EXPECT_EQ(remainder.Length(), available); }); } TEST_P(ParametrizedCryptTest, DummyCipherStrategy_Clone) { using CipherStrategy = DummyCipherStrategy; const TestParams& testParams = GetParam(); // XXX Add deduction guide for RefPtr from already_AddRefed const auto baseOutputStream = WrapNotNull( RefPtr{FixedBufferOutputStream::Create(2048)}); const auto data = MakeTestData(testParams.DataSize()); WriteTestData( nsCOMPtr{baseOutputStream.get()}, Span{data}, testParams.EffectiveWriteChunkSize(), testParams.BlockSize(), CipherStrategy::KeyType{}, testParams.FlushMode()); const auto baseInputStream = MakeRefPtr(baseOutputStream->WrittenData()); const auto inStream = ReadTestData( WrapNotNull(nsCOMPtr{baseInputStream}), Span{data}, testParams.EffectiveReadChunkSize(), testParams.BlockSize(), CipherStrategy::KeyType{}); nsCOMPtr clonedInputStream; EXPECT_EQ(NS_OK, inStream->Clone(getter_AddRefs(clonedInputStream))); ReadTestData( static_cast&>(*clonedInputStream), Span{data}, testParams.EffectiveReadChunkSize()); } // XXX This test is actually only parametrized on the block size. TEST_P(ParametrizedCryptTest, DummyCipherStrategy_IncompleteBlock) { using CipherStrategy = DummyCipherStrategy; const TestParams& testParams = GetParam(); // Provide half a block, content doesn't matter. nsTArray data; data.SetLength(testParams.BlockSize() / 2); const auto baseInputStream = MakeRefPtr(data); const auto inStream = MakeSafeRefPtr>( WrapNotNull(nsCOMPtr{baseInputStream}), testParams.BlockSize(), CipherStrategy::KeyType{}); nsTArray readData; readData.SetLength(testParams.BlockSize()); uint32_t read; EXPECT_EQ(NS_ERROR_CORRUPTED_CONTENT, inStream->Read(reinterpret_cast(readData.Elements()), readData.Length(), &read)); } enum struct SeekOffset { Zero, MinusHalfDataSize, PlusHalfDataSize, PlusDataSize, MinusDataSize }; using SeekOp = std::pair; using PackedSeekTestParams = std::tuple>; struct SeekTestParams { size_t mDataSize; size_t mBlockSize; std::vector 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& 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 {}; TEST_P(ParametrizedSeekCryptTest, DummyCipherStrategy_Seek) { using CipherStrategy = DummyCipherStrategy; const SeekTestParams& testParams = GetParam(); const auto baseOutputStream = WrapNotNull( RefPtr{FixedBufferOutputStream::Create(2048)}); const auto data = MakeTestData(testParams.mDataSize); WriteTestData( nsCOMPtr{baseOutputStream.get()}, Span{data}, testParams.mDataSize, testParams.mBlockSize, CipherStrategy::KeyType{}, FlushMode::Never); const auto baseInputStream = MakeRefPtr(baseOutputStream->WrittenData()); const auto inStream = MakeSafeRefPtr>( WrapNotNull(nsCOMPtr{baseInputStream}), testParams.mBlockSize, 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(dataSize) / 2; case SeekOffset::PlusHalfDataSize: return dataSize / 2; case SeekOffset::MinusDataSize: return -static_cast(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(); readData.SetLength(data.Length()); uint32_t read; EXPECT_EQ(NS_OK, inStream->Read(reinterpret_cast(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_SUITE_P( DOM_Quota_EncryptedStream_Parametrized, ParametrizedCryptTest, testing::Combine( /* dataSize */ testing::Values(0u, 16u, 256u, 512u, 513u), /* writeChunkSize */ testing::Values(ChunkSize::SingleByte, ChunkSize::Unaligned, ChunkSize::DataSize), /* readChunkSize */ testing::Values(ChunkSize::SingleByte, ChunkSize::Unaligned, ChunkSize::DataSize), /* blockSize */ testing::Values(256u, 1024u /*, 8192u*/), /* flushMode */ testing::Values(FlushMode::Never, FlushMode::AfterEachChunk)), TestParamToString); INSTANTIATE_TEST_SUITE_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{{nsISeekableStream::NS_SEEK_SET, SeekOffset::PlusDataSize}}, std::vector{{nsISeekableStream::NS_SEEK_SET, SeekOffset::PlusHalfDataSize}}, /* NS_SEEK_SET only, multiple ops */ std::vector{ {nsISeekableStream::NS_SEEK_SET, SeekOffset::PlusHalfDataSize}, {nsISeekableStream::NS_SEEK_SET, SeekOffset::Zero}}, /* NS_SEEK_CUR only, single ops */ std::vector{ {nsISeekableStream::NS_SEEK_CUR, SeekOffset::Zero}}, std::vector{{nsISeekableStream::NS_SEEK_CUR, SeekOffset::PlusDataSize}}, std::vector{{nsISeekableStream::NS_SEEK_CUR, SeekOffset::PlusHalfDataSize}}, /* NS_SEEK_END only, single ops */ std::vector{ {nsISeekableStream::NS_SEEK_END, SeekOffset::Zero}}, std::vector{{nsISeekableStream::NS_SEEK_END, SeekOffset::MinusDataSize}}, std::vector{{nsISeekableStream::NS_SEEK_END, SeekOffset::MinusHalfDataSize}})), SeekTestParamToString);