Skip to content

Commit c72de99

Browse files
committed
util: add CBufferedFile::SkipTo() to move ahead in the stream
SkipTo() reads data from the file into the CBufferedFile object (memory), but, unlike this object's read() method, SkipTo() doesn't transfer data into a caller's memory buffer. This is useful because after skipping forward in the stream in this way, the user can, if needed, rewind the stream (SetPos()) and access the object's memory buffer including ranges that were skipped over (without needing to read from the disk file).
1 parent 48a6890 commit c72de99

File tree

2 files changed

+96
-19
lines changed

2 files changed

+96
-19
lines changed

src/streams.h

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -612,7 +612,6 @@ class CBufferedFile
612612
uint64_t nRewind; //!< how many bytes we guarantee to rewind
613613
std::vector<std::byte> vchBuf; //!< the buffer
614614

615-
protected:
616615
//! read data from the source to fill the buffer
617616
bool Fill() {
618617
unsigned int pos = nSrcPos % vchBuf.size();
@@ -630,6 +629,28 @@ class CBufferedFile
630629
return true;
631630
}
632631

632+
//! Advance the stream's read pointer (m_read_pos) by up to 'length' bytes,
633+
//! filling the buffer from the file so that at least one byte is available.
634+
//! Return a pointer to the available buffer data and the number of bytes
635+
//! (which may be less than the requested length) that may be accessed
636+
//! beginning at that pointer.
637+
std::pair<std::byte*, size_t> AdvanceStream(size_t length)
638+
{
639+
assert(m_read_pos <= nSrcPos);
640+
if (m_read_pos + length > nReadLimit) {
641+
throw std::ios_base::failure("Attempt to position past buffer limit");
642+
}
643+
// If there are no bytes available, read from the file.
644+
if (m_read_pos == nSrcPos && length > 0) Fill();
645+
646+
size_t buffer_offset{static_cast<size_t>(m_read_pos % vchBuf.size())};
647+
size_t buffer_available{static_cast<size_t>(vchBuf.size() - buffer_offset)};
648+
size_t bytes_until_source_pos{static_cast<size_t>(nSrcPos - m_read_pos)};
649+
size_t advance{std::min({length, buffer_available, bytes_until_source_pos})};
650+
m_read_pos += advance;
651+
return std::make_pair(&vchBuf[buffer_offset], advance);
652+
}
653+
633654
public:
634655
CBufferedFile(FILE* fileIn, uint64_t nBufSize, uint64_t nRewindIn, int nTypeIn, int nVersionIn)
635656
: nType(nTypeIn), nVersion(nVersionIn), nSrcPos(0), m_read_pos(0), nReadLimit(std::numeric_limits<uint64_t>::max()), nRewind(nRewindIn), vchBuf(nBufSize, std::byte{0})
@@ -667,24 +688,21 @@ class CBufferedFile
667688
//! read a number of bytes
668689
void read(Span<std::byte> dst)
669690
{
670-
if (dst.size() + m_read_pos > nReadLimit) {
671-
throw std::ios_base::failure("Read attempted past buffer limit");
672-
}
673691
while (dst.size() > 0) {
674-
if (m_read_pos == nSrcPos)
675-
Fill();
676-
unsigned int pos = m_read_pos % vchBuf.size();
677-
size_t nNow = dst.size();
678-
if (nNow + pos > vchBuf.size())
679-
nNow = vchBuf.size() - pos;
680-
if (nNow + m_read_pos > nSrcPos)
681-
nNow = nSrcPos - m_read_pos;
682-
memcpy(dst.data(), &vchBuf[pos], nNow);
683-
m_read_pos += nNow;
684-
dst = dst.subspan(nNow);
692+
auto [buffer_pointer, length]{AdvanceStream(dst.size())};
693+
memcpy(dst.data(), buffer_pointer, length);
694+
dst = dst.subspan(length);
685695
}
686696
}
687697

698+
//! Move the read position ahead in the stream to the given position.
699+
//! Use SetPos() to back up in the stream, not SkipTo().
700+
void SkipTo(const uint64_t file_pos)
701+
{
702+
assert(file_pos >= m_read_pos);
703+
while (m_read_pos < file_pos) AdvanceStream(file_pos - m_read_pos);
704+
}
705+
688706
//! return the current reading position
689707
uint64_t GetPos() const {
690708
return m_read_pos;

src/test/streams_tests.cpp

Lines changed: 63 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ BOOST_AUTO_TEST_CASE(streams_buffered_file)
253253
BOOST_CHECK(false);
254254
} catch (const std::exception& e) {
255255
BOOST_CHECK(strstr(e.what(),
256-
"Read attempted past buffer limit") != nullptr);
256+
"Attempt to position past buffer limit") != nullptr);
257257
}
258258
// The default argument removes the limit completely.
259259
BOOST_CHECK(bf.SetLimit());
@@ -322,14 +322,63 @@ BOOST_AUTO_TEST_CASE(streams_buffered_file)
322322
BOOST_CHECK(!bf.SetPos(0));
323323
// But we should now be positioned at least as far back as allowed
324324
// by the rewind window (relative to our farthest read position, 40).
325-
BOOST_CHECK(bf.GetPos() <= 30);
325+
BOOST_CHECK(bf.GetPos() <= 30U);
326326

327327
// We can explicitly close the file, or the destructor will do it.
328328
bf.fclose();
329329

330330
fs::remove(streams_test_filename);
331331
}
332332

333+
BOOST_AUTO_TEST_CASE(streams_buffered_file_skip)
334+
{
335+
fs::path streams_test_filename = m_args.GetDataDirBase() / "streams_test_tmp";
336+
FILE* file = fsbridge::fopen(streams_test_filename, "w+b");
337+
// The value at each offset is the byte offset (e.g. byte 1 in the file has the value 0x01).
338+
for (uint8_t j = 0; j < 40; ++j) {
339+
fwrite(&j, 1, 1, file);
340+
}
341+
rewind(file);
342+
343+
// The buffer is 25 bytes, allow rewinding 10 bytes.
344+
CBufferedFile bf(file, 25, 10, 222, 333);
345+
346+
uint8_t i;
347+
// This is like bf >> (7-byte-variable), in that it will cause data
348+
// to be read from the file into memory, but it's not copied to us.
349+
bf.SkipTo(7);
350+
BOOST_CHECK_EQUAL(bf.GetPos(), 7U);
351+
bf >> i;
352+
BOOST_CHECK_EQUAL(i, 7);
353+
354+
// The bytes in the buffer up to offset 7 are valid and can be read.
355+
BOOST_CHECK(bf.SetPos(0));
356+
bf >> i;
357+
BOOST_CHECK_EQUAL(i, 0);
358+
bf >> i;
359+
BOOST_CHECK_EQUAL(i, 1);
360+
361+
bf.SkipTo(11);
362+
bf >> i;
363+
BOOST_CHECK_EQUAL(i, 11);
364+
365+
// SkipTo() honors the transfer limit; we can't position beyond the limit.
366+
bf.SetLimit(13);
367+
try {
368+
bf.SkipTo(14);
369+
BOOST_CHECK(false);
370+
} catch (const std::exception& e) {
371+
BOOST_CHECK(strstr(e.what(), "Attempt to position past buffer limit") != nullptr);
372+
}
373+
374+
// We can position exactly to the transfer limit.
375+
bf.SkipTo(13);
376+
BOOST_CHECK_EQUAL(bf.GetPos(), 13U);
377+
378+
bf.fclose();
379+
fs::remove(streams_test_filename);
380+
}
381+
333382
BOOST_AUTO_TEST_CASE(streams_buffered_file_rand)
334383
{
335384
// Make this test deterministic.
@@ -361,7 +410,7 @@ BOOST_AUTO_TEST_CASE(streams_buffered_file_rand)
361410
// sizes; the boundaries of the objects can interact arbitrarily
362411
// with the CBufferFile's internal buffer. These first three
363412
// cases simulate objects of various sizes (1, 2, 5 bytes).
364-
switch (InsecureRandRange(5)) {
413+
switch (InsecureRandRange(6)) {
365414
case 0: {
366415
uint8_t a[1];
367416
if (currentPos + 1 > fileSize)
@@ -399,6 +448,16 @@ BOOST_AUTO_TEST_CASE(streams_buffered_file_rand)
399448
break;
400449
}
401450
case 3: {
451+
// SkipTo is similar to the "read" cases above, except
452+
// we don't receive the data.
453+
size_t skip_length{static_cast<size_t>(InsecureRandRange(5))};
454+
if (currentPos + skip_length > fileSize) continue;
455+
bf.SetLimit(currentPos + skip_length);
456+
bf.SkipTo(currentPos + skip_length);
457+
currentPos += skip_length;
458+
break;
459+
}
460+
case 4: {
402461
// Find a byte value (that is at or ahead of the current position).
403462
size_t find = currentPos + InsecureRandRange(8);
404463
if (find >= fileSize)
@@ -415,7 +474,7 @@ BOOST_AUTO_TEST_CASE(streams_buffered_file_rand)
415474
currentPos++;
416475
break;
417476
}
418-
case 4: {
477+
case 5: {
419478
size_t requestPos = InsecureRandRange(maxPos + 4);
420479
bool okay = bf.SetPos(requestPos);
421480
// The new position may differ from the requested position

0 commit comments

Comments
 (0)