// Copyright (C) 2012-2024 Internet Systems Consortium, Inc. ("ISC")
//
// 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 <config.h>

#include <exceptions/isc_assert.h>
#include <dns/master_lexer_inputsource.h>
#include <dns/master_lexer.h>

#include <istream>
#include <iostream>
#include <cerrno>
#include <cstring>

namespace isc {
namespace dns {
namespace master_lexer_internal {

namespace { // unnamed namespace

std::string
createStreamName(const std::istream& input_stream) {
     std::stringstream ss;
     ss << "stream-" << &input_stream;
     return (ss.str());
}

size_t
getStreamSize(std::istream& is) {
    errno = 0;                  // see below
    is.seekg(0, std::ios_base::end);
    if (is.bad()) {
        // This means the istream has an integrity error.  It doesn't make
        // sense to continue from this point, so we treat it as a fatal error.
        isc_throw(InputSource::OpenError,
                  "failed to seek end of input source");
    } else if (is.fail() || errno != 0) {
        // This is an error specific to seekg().  There can be several
        // reasons, but the most likely cause in this context is that the
        // stream is associated with a special type of file such as a pipe.
        // In this case, it's more likely that other main operations of
        // the input source work fine, so we continue with just setting
        // the stream size to "unknown".
        //
        // (At least some versions of) Solaris + SunStudio shows deviant
        // behavior here: seekg() apparently calls lseek(2) internally, but
        // even if it fails it doesn't set the error bits of istream. That will
        // confuse the rest of this function, so, as a heuristic workaround
        // we check errno and handle any non 0 value as fail().
        is.clear();   // clear this error not to confuse later ops.
        return (MasterLexer::SOURCE_SIZE_UNKNOWN);
    }
    const std::streampos len = is.tellg();
    size_t ret = len;
    if (len == static_cast<std::streampos>(-1)) { // cast for some compilers
        if (!is.fail()) {
            // tellg() returns -1 if istream::fail() would be true, but it's
            // not guaranteed that it shouldn't be returned in other cases.
            // In fact, with the combination of SunStudio and stlport,
            // a stringstream created by the default constructor showed that
            // behavior.  We treat such cases as an unknown size.
            ret = MasterLexer::SOURCE_SIZE_UNKNOWN;
        } else {
            isc_throw(InputSource::OpenError, "failed to get input size");
        }
    }
    is.seekg(0, std::ios::beg);
    if (is.fail()) {
        isc_throw(InputSource::OpenError,
                  "failed to seek beginning of input source");
    }
    isc_throw_assert(len >= 0 || ret == MasterLexer::SOURCE_SIZE_UNKNOWN);
    return (ret);
}

} // end of unnamed namespace

// Explicit definition of class static constant.  The value is given in the
// declaration so it's not needed here.
const int InputSource::END_OF_STREAM;

InputSource::InputSource(std::istream& input_stream) :
    at_eof_(false),
    line_(1),
    saved_line_(line_),
    buffer_pos_(0),
    total_pos_(0),
    name_(createStreamName(input_stream)),
    input_(input_stream),
    input_size_(getStreamSize(input_)) {
}

namespace {
// A helper to initialize InputSource::input_ in the member initialization
// list.
std::istream&
openFileStream(std::ifstream& file_stream, const char* filename) {
    errno = 0;
    file_stream.open(filename);
    if (file_stream.fail()) {
        std::string error_txt("Error opening the input source file: ");
        error_txt += filename;
        if (errno != 0) {
            error_txt += "; possible cause: ";
            error_txt += std::strerror(errno);
        }
        isc_throw(InputSource::OpenError, error_txt);
    }

    return (file_stream);
}
}

InputSource::InputSource(const char* filename) :
    at_eof_(false),
    line_(1),
    saved_line_(line_),
    buffer_pos_(0),
    total_pos_(0),
    name_(filename),
    input_(openFileStream(file_stream_, filename)),
    input_size_(getStreamSize(input_)) {
}

InputSource::~InputSource() {
    if (file_stream_.is_open()) {
        file_stream_.close();
    }
}

int
InputSource::getChar() {
    if (buffer_pos_ == buffer_.size()) {
        // We may have reached EOF at the last call to
        // getChar(). at_eof_ will be set then. We then simply return
        // early.
        if (at_eof_) {
            return (END_OF_STREAM);
        }
        // We are not yet at EOF. Read from the stream.
        const int c = input_.get();
        // Have we reached EOF now? If so, set at_eof_ and return early,
        // but don't modify buffer_pos_ (which should still be equal to
        // the size of buffer_).
        if (input_.eof()) {
            at_eof_ = true;
            return (END_OF_STREAM);
        }
        // This has to come after the .eof() check as some
        // implementations seem to check the eofbit also in .fail().
        if (input_.fail()) {
            isc_throw(MasterLexer::ReadError,
                      "Error reading from the input stream: " << getName());
        }
        buffer_.push_back(c);
    }

    const int c = buffer_[buffer_pos_];
    ++buffer_pos_;
    ++total_pos_;
    if (c == '\n') {
        ++line_;
    }

    return (c);
}

void
InputSource::ungetChar() {
    if (at_eof_) {
        at_eof_ = false;
    } else if (buffer_pos_ == 0) {
        isc_throw(UngetBeforeBeginning,
                  "Cannot skip before the start of buffer");
    } else {
        --buffer_pos_;
        --total_pos_;
        if (buffer_[buffer_pos_] == '\n') {
            --line_;
        }
    }
}

void
InputSource::ungetAll() {
    isc_throw_assert(total_pos_ >= buffer_pos_);
    total_pos_ -= buffer_pos_;
    buffer_pos_ = 0;
    line_ = saved_line_;
    at_eof_ = false;
}

void
InputSource::saveLine() {
    saved_line_ = line_;
}

void
InputSource::compact() {
    if (buffer_pos_ == buffer_.size()) {
        buffer_.clear();
    } else {
        buffer_.erase(buffer_.begin(), buffer_.begin() + buffer_pos_);
    }

    buffer_pos_ = 0;
}

void
InputSource::mark() {
    saveLine();
    compact();
}

} // namespace master_lexer_internal
} // namespace dns
} // namespace isc
