LCOV - code coverage report
Current view: top level - io - BufferedTextReader.cpp (source / functions) Hit Total Coverage
Test: coverage_filtered.info Lines: 85 98 86.7 %
Date: 2025-06-15 16:18:27 Functions: 10 10 100.0 %

          Line data    Source code
       1             : /* Copyright (C) 2025 Robert Schiwon
       2             :  *
       3             :  * This file is part of libjdx.
       4             :  *
       5             :  * libjdx is free software: you can redistribute it and/or modify it under the
       6             :  * terms of the GNU Lesser General Public License as published by the Free
       7             :  * Software Foundation, either version 3 of the License, or (at your option) any
       8             :  * later version.
       9             :  *
      10             :  * libjdx is distributed in the hope that it will be useful, but WITHOUT ANY
      11             :  * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
      12             :  * A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
      13             :  * details.
      14             :  *
      15             :  * You should have received a copy of the GNU Lesser General Public License
      16             :  * along with libjdx. If not, see <https://www.gnu.org/licenses/>.
      17             :  */
      18             : #include "io/BufferedTextReader.hpp"
      19             : 
      20             : #include <algorithm>
      21             : #include <fstream>
      22             : #include <istream>
      23             : #include <sstream>
      24             : #include <stdexcept>
      25             : #include <vector>
      26             : 
      27           2 : libjdx::io::BufferedTextReader::BufferedTextReader(
      28           2 :     std::unique_ptr<std::istream> streamPtr, size_t bufferSize)
      29           2 :     : m_streamPtr{std::move(streamPtr)}
      30             :     , m_bufferMaxSize{bufferSize}
      31             :     , m_buffer(bufferSize)
      32             :     , m_bufferBasePos{0}
      33           2 :     , m_bufferPosIt{m_buffer.begin()}
      34             : {
      35           2 :     setStreamFlags();
      36           2 :     updateBuffer(0);
      37           2 : }
      38             : 
      39           1 : libjdx::io::BufferedTextReader::BufferedTextReader(
      40           1 :     const std::string& filePath, size_t bufferSize)
      41           1 :     : m_streamPtr{std::make_unique<std::ifstream>(filePath)}
      42             :     , m_bufferMaxSize{bufferSize}
      43             :     , m_buffer(bufferSize)
      44             :     , m_bufferBasePos{0}
      45           2 :     , m_bufferPosIt{m_buffer.begin()}
      46             : {
      47           1 :     setStreamFlags();
      48           1 :     updateBuffer(0);
      49           1 : }
      50             : 
      51           3 : void libjdx::io::BufferedTextReader::setStreamFlags()
      52             : {
      53           3 :     if (m_streamPtr == nullptr)
      54             :     {
      55           0 :         throw std::runtime_error("Text reader input stream is null.");
      56             :     }
      57             :     // the underlying read() method sets failbit and eofbit at end of file, so
      58             :     // do not set std::ios::failbit std::ios::eofbit
      59           3 :     m_streamPtr->exceptions(std::ios::badbit);
      60           3 : }
      61             : 
      62           4 : std::ios::pos_type libjdx::io::BufferedTextReader::calculateAbsolutePosition(
      63             :     std::ios::pos_type position, std::ios_base::seekdir seekdir)
      64             : {
      65             :     // find absolute stream position
      66           4 :     std::streampos pos = 0;
      67           4 :     if (seekdir == std::ios_base::beg)
      68             :     {
      69           4 :         pos = position;
      70             :     }
      71           0 :     else if (seekdir == std::ios_base::cur)
      72             :     {
      73             :         pos = m_bufferBasePos
      74           0 :               + static_cast<std::ios::pos_type>(
      75           0 :                   std::distance(m_buffer.cbegin(), m_bufferPosIt))
      76           0 :               + position;
      77             :     }
      78           0 :     else if (seekdir == std::ios_base::end)
      79             :     {
      80           0 :         pos = getLength() - position;
      81             :     }
      82           4 :     return pos;
      83             : }
      84             : 
      85          15 : void libjdx::io::BufferedTextReader::updateBuffer(std::ios::pos_type position)
      86             : {
      87             :     // NOLINTBEGIN(bugprone-narrowing-conversions,cppcoreguidelines-narrowing-conversions)
      88             :     auto bufferStartPos = static_cast<std::ios::pos_type>(
      89          15 :         (position / m_bufferMaxSize) * m_bufferMaxSize);
      90          15 :     m_streamPtr->seekg(bufferStartPos);
      91          15 :     m_buffer.resize(m_bufferMaxSize);
      92          15 :     m_streamPtr->read(m_buffer.data(), m_bufferMaxSize);
      93          15 :     const auto numCharsRead = m_streamPtr->gcount();
      94          15 :     if (m_streamPtr->eof())
      95             :     {
      96           3 :         m_streamPtr->clear();
      97             :     }
      98          15 :     m_buffer.resize(numCharsRead);
      99          15 :     m_bufferBasePos = bufferStartPos;
     100          30 :     m_bufferPosIt = std::begin(m_buffer)
     101          15 :                     + static_cast<std::vector<char>::difference_type>(
     102          30 :                         position - bufferStartPos);
     103             :     // NOLINTEND(bugprone-narrowing-conversions,cppcoreguidelines-narrowing-conversions)
     104          15 : }
     105             : 
     106          11 : std::ios::pos_type libjdx::io::BufferedTextReader::tellg() const
     107             : {
     108             :     return m_bufferBasePos
     109          22 :            + static_cast<std::ios::pos_type>(
     110          22 :                std::distance(m_buffer.cbegin(), m_bufferPosIt));
     111             : }
     112             : 
     113           4 : void libjdx::io::BufferedTextReader::seekg(
     114             :     std::ios::off_type position, std::ios_base::seekdir seekdir)
     115             : {
     116           4 :     auto pos = calculateAbsolutePosition(position, seekdir);
     117             :     // NOLINTBEGIN(bugprone-narrowing-conversions,cppcoreguidelines-narrowing-conversions)
     118           4 :     if (pos >= m_bufferBasePos
     119           5 :         && pos < m_bufferBasePos
     120           5 :                      + static_cast<std::ios::pos_type>(m_buffer.size()))
     121             :     // NOLINTEND(bugprone-narrowing-conversions,cppcoreguidelines-narrowing-conversions)
     122             :     {
     123             :         // new pos inside existing buffer => only update m_bufferPosIt
     124           0 :         auto newBufferPos = position - m_bufferBasePos;
     125             :         m_bufferPosIt
     126           0 :             = m_buffer.cbegin()
     127           0 :               + static_cast<std::vector<char>::difference_type>(newBufferPos);
     128             :     }
     129             :     else
     130             :     {
     131             :         const auto bufferEndPos
     132             :             = m_bufferBasePos
     133           4 :               + static_cast<std::ios::pos_type>(
     134           4 :                   std::distance(m_buffer.cbegin(), m_buffer.cend()));
     135           4 :         if (bufferEndPos == pos && pos == getLength())
     136             :         {
     137             :             // buffer already at end of stream => noop
     138           0 :             m_bufferPosIt = m_buffer.cend();
     139             :         }
     140             :         else
     141             :         {
     142             :             // new pos outside existing buffer => read chunk around new pos from
     143             :             // stream
     144           4 :             updateBuffer(pos);
     145             :         }
     146             :     }
     147           4 : }
     148             : 
     149          13 : std::ios::pos_type libjdx::io::BufferedTextReader::getLength()
     150             : {
     151          13 :     const std::ios::pos_type current = m_streamPtr->tellg();
     152          13 :     m_streamPtr->seekg(0, std::ios::end);
     153          13 :     const std::ios::pos_type length = m_streamPtr->tellg();
     154          13 :     m_streamPtr->seekg(current, std::ios::beg);
     155          13 :     return length;
     156             : }
     157             : 
     158           6 : bool libjdx::io::BufferedTextReader::eof() const
     159             : {
     160             :     // static cast is safe as distance from the beginning cannot benegative
     161           6 :     if (static_cast<size_t>(std::distance(m_buffer.cbegin(), m_bufferPosIt))
     162           6 :         < m_buffer.size())
     163             :     {
     164           3 :         return false;
     165             :     }
     166             :     // see:
     167             :     // https://stackoverflow.com/questions/6283632/how-to-know-if-the-next-character-is-eof-in-c
     168           3 :     auto c = m_streamPtr->peek();
     169           3 :     if (c == EOF)
     170             :     {
     171           2 :         if (m_streamPtr->eof())
     172             :         {
     173             :             // otherwise operations no longer works after eofbit is set
     174           2 :             m_streamPtr->clear();
     175           2 :             return true;
     176             :         }
     177             :         throw std::runtime_error(
     178           0 :             "End of file reached, but std::ios::eofbit not set.");
     179             :     }
     180           1 :     return false;
     181             : }
     182             : 
     183           6 : std::string libjdx::io::BufferedTextReader::readLine()
     184             : {
     185             :     // NOLINTBEGIN(bugprone-narrowing-conversions,cppcoreguidelines-narrowing-conversions)
     186             :     auto nextChunkStartPos
     187           6 :         = m_bufferBasePos + static_cast<std::ios::pos_type>(m_bufferMaxSize);
     188           6 :     if (m_bufferPosIt == m_buffer.cend() && nextChunkStartPos >= getLength())
     189             :     {
     190           1 :         throw std::runtime_error("Error reading line from istream.");
     191             :     }
     192           5 :     auto posIt = std::find(m_bufferPosIt, m_buffer.cend(), '\n');
     193           5 :     std::string out{m_bufferPosIt, posIt};
     194           5 :     auto lfFound = posIt != m_buffer.cend();
     195             :     // set new buffer position either past end or past found LF
     196           5 :     m_bufferPosIt = lfFound ? ++posIt : m_buffer.cend();
     197          13 :     while (!lfFound && nextChunkStartPos < getLength())
     198             :     {
     199             :         // no LF encountered => load next chunk if available and continue search
     200           8 :         updateBuffer(nextChunkStartPos);
     201             :         nextChunkStartPos = m_bufferBasePos
     202           8 :                             + static_cast<std::ios::pos_type>(m_bufferMaxSize);
     203           8 :         posIt = std::find(m_bufferPosIt, m_buffer.cend(), '\n');
     204           8 :         out.append(m_bufferPosIt, posIt);
     205           8 :         lfFound = posIt != m_buffer.cend();
     206             :         // set new buffer position either past end or past found LF
     207           8 :         m_bufferPosIt = lfFound ? ++posIt : m_buffer.cend();
     208             :     }
     209             :     // NOLINTEND(bugprone-narrowing-conversions,cppcoreguidelines-narrowing-conversions)
     210           5 :     if (m_streamPtr->eof())
     211             :     {
     212             :         // cleat eofbit, so that other operations will still succeed
     213             :         // other types of errors raise exceptions
     214           0 :         m_streamPtr->clear();
     215             :     }
     216           5 :     if (!out.empty() && out.at(out.size() - 1) == '\r')
     217             :     {
     218             :         // remove trailing \r in case line ending is \r\n and has not been
     219             :         // converted to \n by stream already
     220           1 :         out.erase(out.size() - 1);
     221             :     }
     222          10 :     return out;
     223             : }

Generated by: LCOV version 1.14