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 : }