// Copyright (C) 2013-2025 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 <dhcp/dhcp4.h>
#include <dhcp/option4_client_fqdn.h>
#include <dns/labelsequence.h>
#include <util/buffer.h>
#include <util/str.h>
#include <sstream>

namespace isc {
namespace dhcp {

/// @brief Implements the logic for the Option6ClientFqdn class.
///
/// The purpose of the class is to separate the implementation details
/// of the Option4ClientFqdn class from the interface. This implementation
/// uses libdns classes to process FQDNs. At some point it may be
/// desired to split libdhcp++ from libdns. In such case the
/// implementation of this class may be changed. The declaration of the
/// Option6ClientFqdn class holds the pointer to implementation, so
/// the transition to a different implementation would not affect the
/// header file.
class Option4ClientFqdnImpl {
public:
    /// Holds flags carried by the option.
    uint8_t flags_;
    /// Holds RCODE1 and RCODE2 values.
    Option4ClientFqdn::Rcode rcode1_;
    Option4ClientFqdn::Rcode rcode2_;
    /// Holds the pointer to a domain name carried in the option.
    boost::shared_ptr<isc::dns::Name> domain_name_;
    /// Indicates whether domain name is partial or fully qualified.
    Option4ClientFqdn::DomainNameType domain_name_type_;

    /// @brief Constructor, from domain name.
    ///
    /// @param flags A value of the flags option field.
    /// @param rcode An object representing the RCODE1 and RCODE2 values.
    /// @param domain_name A domain name carried by the option given in the
    /// textual format.
    /// @param name_type A value which indicates whether domain-name is partial
    /// or fully qualified.
    Option4ClientFqdnImpl(const uint8_t flags,
                          const Option4ClientFqdn::Rcode& rcode,
                          const std::string& domain_name,
                          const Option4ClientFqdn::DomainNameType name_type);

    /// @brief Constructor, from wire data.
    ///
    /// @param first An iterator pointing to the beginning of the option data
    /// in the wire format.
    /// @param last An iterator pointing to the end of the option data in the
    /// wire format.
    Option4ClientFqdnImpl(OptionBufferConstIter first,
                          OptionBufferConstIter last);

    /// @brief Copy constructor.
    ///
    /// @param source An object being copied.
    Option4ClientFqdnImpl(const Option4ClientFqdnImpl& source);

    /// @brief Assignment operator.
    ///
    /// @param source An object which is being assigned.
    Option4ClientFqdnImpl& operator=(const Option4ClientFqdnImpl& source);

    /// @brief Set a new domain name for the option.
    ///
    /// @param domain_name A new domain name to be assigned.
    /// @param name_type A value which indicates whether the domain-name is
    /// partial or fully qualified.
    void setDomainName(const std::string& domain_name,
                       const Option4ClientFqdn::DomainNameType name_type);

    /// @brief Check if flags are valid.
    ///
    /// In particular, this function checks if the N and S bits are not
    /// set to 1 in the same time.
    ///
    /// @param flags A value carried by the flags field of the option.
    /// @param check_mbz A boolean value which indicates if this function should
    /// check if the MBZ bits are set (if true). This parameter should be set
    /// to false when validating flags in the received message. This is because
    /// server should ignore MBZ bits in received messages.
    /// @throw InvalidOption6FqdnFlags if flags are invalid.
    static void checkFlags(const uint8_t flags, const bool check_mbz);

    /// @brief Parse the Option provided in the wire format.
    ///
    /// @param first An iterator pointing to the beginning of the option data
    /// in the wire format.
    /// @param last An iterator pointing to the end of the option data in the
    /// wire format.
    void parseWireData(OptionBufferConstIter first,
                       OptionBufferConstIter last);

    /// @brief Parse domain-name encoded in the canonical format.
    ///
    void parseCanonicalDomainName(OptionBufferConstIter first,
                                  OptionBufferConstIter last);

    /// @brief Parse domain-name encoded in the deprecated ASCII format.
    ///
    /// @param first An iterator pointing to the beginning of the option data
    /// where domain-name is stored.
    /// @param last An iterator pointing to the end of the option data where
    /// domain-name is stored.
    void parseASCIIDomainName(OptionBufferConstIter first,
                              OptionBufferConstIter last);

};

Option4ClientFqdnImpl::
Option4ClientFqdnImpl(const uint8_t flags,
                      const Option4ClientFqdn::Rcode& rcode,
                      const std::string& domain_name,
                      // cppcheck 1.57 complains that const enum value is not passed
                      // by reference. Note that, it accepts the non-const enum value
                      // to be passed by value. In both cases it is unnecessary to
                      // pass the enum by reference.
                      // cppcheck-suppress passedByValue
                      const Option4ClientFqdn::DomainNameType name_type)
    : flags_(flags),
      rcode1_(rcode),
      rcode2_(rcode),
      domain_name_(),
      domain_name_type_(name_type) {

    //  Check if flags are correct. Also, check that MBZ bits are not set. If
    // they are, throw exception.
    checkFlags(flags_, true);
    // Set domain name. It may throw an exception if domain name has wrong
    // format.
    setDomainName(domain_name, name_type);
}

Option4ClientFqdnImpl::Option4ClientFqdnImpl(OptionBufferConstIter first,
                                             OptionBufferConstIter last)
    : rcode1_(Option4ClientFqdn::RCODE_CLIENT()),
      rcode2_(Option4ClientFqdn::RCODE_CLIENT()),
      domain_name_type_(Option4ClientFqdn::PARTIAL) {
    parseWireData(first, last);
    // Verify that flags value was correct. This constructor is used to parse
    // incoming packet, so don't check MBZ bits. They are ignored because we
    // don't want to discard the whole option because MBZ bits are set.
    checkFlags(flags_, false);
}

Option4ClientFqdnImpl::
Option4ClientFqdnImpl(const Option4ClientFqdnImpl& source)
    : flags_(source.flags_),
      rcode1_(source.rcode1_),
      rcode2_(source.rcode2_),
      domain_name_(),
      domain_name_type_(source.domain_name_type_) {
    if (source.domain_name_) {
        domain_name_.reset(new isc::dns::Name(*source.domain_name_));
    }
}

Option4ClientFqdnImpl&
// This assignment operator handles assignment to self, it copies all
// required values.
// cppcheck-suppress operatorEqToSelf
Option4ClientFqdnImpl::operator=(const Option4ClientFqdnImpl& source) {
    if (source.domain_name_) {
        domain_name_.reset(new isc::dns::Name(*source.domain_name_));

    } else {
        domain_name_.reset();
    }

    // Assignment is exception safe.
    flags_ = source.flags_;
    rcode1_ = source.rcode1_;
    rcode2_ = source.rcode2_;
    domain_name_type_ = source.domain_name_type_;

    return (*this);
}

void
Option4ClientFqdnImpl::
setDomainName(const std::string& domain_name,
              // cppcheck 1.57 complains that const enum value is not passed
              // by reference. Note that, it accepts the non-const enum
              // to be passed by value. In both cases it is unnecessary to
              // pass the enum by reference.
              // cppcheck-suppress passedByValue
              const Option4ClientFqdn::DomainNameType name_type) {
    // domain-name must be trimmed. Otherwise, string comprising spaces only
    // would be treated as a fully qualified name.
    std::string name = isc::util::str::trim(domain_name);
    if (name.empty()) {
        if (name_type == Option4ClientFqdn::FULL) {
            isc_throw(InvalidOption4FqdnDomainName,
                      "fully qualified domain-name must not be empty"
                      << " when setting new domain-name for DHCPv4 Client"
                      << " FQDN Option");
        }
        // The special case when domain-name is empty is marked by setting the
        // pointer to the domain-name object to NULL.
        domain_name_.reset();

    } else {
        try {
            // The second argument indicates that the name should be converted
            // to lower case.
            domain_name_.reset(new isc::dns::Name(name, true));

        } catch (const Exception&) {
            isc_throw(InvalidOption4FqdnDomainName,
                      "invalid domain-name value '"
                      << domain_name << "' when setting new domain-name for"
                      << " DHCPv4 Client FQDN Option");

        }
    }

    domain_name_type_ = name_type;
}

void
Option4ClientFqdnImpl::checkFlags(const uint8_t flags, const bool check_mbz) {
    // The Must Be Zero (MBZ) bits must not be set.
    if (check_mbz && ((flags & ~Option4ClientFqdn::FLAG_MASK) != 0)) {
        isc_throw(InvalidOption4FqdnFlags,
                  "invalid DHCPv4 Client FQDN Option flags: 0x"
                  << std::hex << static_cast<int>(flags) << std::dec);
    }

    // According to RFC 4702, section 2.1. if the N bit is 1, the S bit
    // MUST be 0. Checking it here.
    if ((flags & (Option4ClientFqdn::FLAG_N | Option4ClientFqdn::FLAG_S))
        == (Option4ClientFqdn::FLAG_N | Option4ClientFqdn::FLAG_S)) {
        isc_throw(InvalidOption4FqdnFlags,
                  "both N and S flag of the DHCPv4 Client FQDN Option are set."
                  << " According to RFC 4702, if the N bit is 1 the S bit"
                  << " MUST be 0");
    }
}

void
Option4ClientFqdnImpl::parseWireData(OptionBufferConstIter first,
                                     OptionBufferConstIter last) {

    // Buffer must comprise at least one byte with the flags.
    // The domain-name may be empty.
    if (std::distance(first, last) < Option4ClientFqdn::FIXED_FIELDS_LEN) {
        isc_throw(OutOfRange, "DHCPv4 Client FQDN Option ("
                  << DHO_FQDN << ") is truncated");
    }

    // Parse flags
    flags_ = *(first++);

    // Parse RCODE1 and RCODE2.
    rcode1_ = Option4ClientFqdn::Rcode(*(first++));
    rcode2_ = Option4ClientFqdn::Rcode(*(first++));

    try {
        if ((flags_ & Option4ClientFqdn::FLAG_E) != 0) {
            parseCanonicalDomainName(first, last);

        } else {
            parseASCIIDomainName(first, last);

        }
    } catch (const Exception& ex) {
        std::ostringstream errmsg;
        errmsg << "failed to parse the domain-name in DHCPv4 Client FQDN "
               << " Option: " << ex.what();
        if (Option::lenient_parsing_) {
            isc_throw(SkipThisOptionError, errmsg.str());
        } else {
            isc_throw(InvalidOption4FqdnDomainName, errmsg.str());
        }
    }
}

void
Option4ClientFqdnImpl::parseCanonicalDomainName(OptionBufferConstIter first,
                                                OptionBufferConstIter last) {
    // Parse domain-name if any.
    if (std::distance(first, last) > 0) {
        // The FQDN may comprise a partial domain-name. In this case it lacks
        // terminating 0. If this is the case, we will need to add zero at
        // the end because Name object constructor requires it.
        if (*(last - 1) != 0) {
            // Create temporary buffer and add terminating zero.
            OptionBuffer buf(first, last);
            buf.push_back(0);
            // Reset domain name.
            isc::util::InputBuffer name_buf(&buf[0], buf.size());
            // The second argument indicates that the name should be converted
            // to lower case.
            domain_name_.reset(new isc::dns::Name(name_buf, true));
            // Terminating zero was missing, so set the domain-name type
            // to partial.
            domain_name_type_ = Option4ClientFqdn::PARTIAL;
        } else {
            // We are dealing with fully qualified domain name so there is
            // no need to add terminating zero. Simply pass the buffer to
            // Name object constructor.
            isc::util::InputBuffer name_buf(&(*first),
                                            std::distance(first, last));
            // The second argument indicates that the name should be converted
            // to lower case.
            domain_name_.reset(new isc::dns::Name(name_buf, true));
            // Set the domain-type to fully qualified domain name.
            domain_name_type_ = Option4ClientFqdn::FULL;
        }
    }
}

void
Option4ClientFqdnImpl::parseASCIIDomainName(OptionBufferConstIter first,
                                            OptionBufferConstIter last) {
    if (std::distance(first, last) > 0) {
        std::string domain_name(first, last);
        // The second argument indicates that the name should be converted
        // to lower case.
        domain_name_.reset(new isc::dns::Name(domain_name, true));
        domain_name_type_ = domain_name[domain_name.length() - 1] == '.' ?
            Option4ClientFqdn::FULL : Option4ClientFqdn::PARTIAL;
    }
}

Option4ClientFqdn::Option4ClientFqdn(const uint8_t flag, const Rcode& rcode)
    : Option(Option::V4, DHO_FQDN),
      impl_(new Option4ClientFqdnImpl(flag, rcode, "", PARTIAL)) {
}

Option4ClientFqdn::Option4ClientFqdn(const uint8_t flag,
                                     const Rcode& rcode,
                                     const std::string& domain_name,
                                     const DomainNameType domain_name_type)
    : Option(Option::V4, DHO_FQDN),
      impl_(new Option4ClientFqdnImpl(flag, rcode, domain_name,
                                      domain_name_type)) {
}

Option4ClientFqdn::Option4ClientFqdn(OptionBufferConstIter first,
                                     OptionBufferConstIter last)
    : Option(Option::V4, DHO_FQDN, first, last),
      impl_(new Option4ClientFqdnImpl(first, last)) {
}

Option4ClientFqdn::~Option4ClientFqdn() {
    delete (impl_);
}

Option4ClientFqdn::Option4ClientFqdn(const Option4ClientFqdn& source)
    : Option(source),
      impl_(new Option4ClientFqdnImpl(*source.impl_)) {
}

OptionPtr
Option4ClientFqdn::clone() const {
    return (cloneInternal<Option4ClientFqdn>());
}

Option4ClientFqdn&
// This assignment operator handles assignment to self, it uses copy
// constructor of Option4ClientFqdnImpl to copy all required values.
// cppcheck-suppress operatorEqToSelf
Option4ClientFqdn::operator=(const Option4ClientFqdn& source) {
    Option::operator=(source);
    Option4ClientFqdnImpl* old_impl = impl_;
    impl_ = new Option4ClientFqdnImpl(*source.impl_);
    delete(old_impl);
    return (*this);
}

bool
Option4ClientFqdn::getFlag(const uint8_t flag) const {
    // Caller should query for one of the: E, N, S or O flags. Any other value
    /// is invalid and results in the exception.
    if (flag != FLAG_S && flag != FLAG_O && flag != FLAG_N && flag != FLAG_E) {
        isc_throw(InvalidOption4FqdnFlags, "invalid DHCPv4 Client FQDN"
                  << " Option flag specified, expected E, N, S or O");
    }

    return ((impl_->flags_ & flag) != 0);
}

void
Option4ClientFqdn::setFlag(const uint8_t flag, const bool set_flag) {
    // Check that flag is in range between 0x1 and 0x7. Although it is
    // discouraged this check doesn't preclude the caller from setting
    // multiple flags concurrently.
    if (((flag & ~FLAG_MASK) != 0) || (flag == 0)) {
        isc_throw(InvalidOption4FqdnFlags, "invalid DHCPv4 Client FQDN"
                  << " Option flag 0x" << std::hex
                  << static_cast<int>(flag) << std::dec
                  << " is being set. Expected combination of E, N, S and O");
    }

    // Copy the current flags into local variable. That way we will be able
    // to test new flags settings before applying them.
    uint8_t new_flag = impl_->flags_;
    if (set_flag) {
        new_flag |= flag;
    } else {
        new_flag &= ~flag;
    }

    // Check new flags. If they are valid, apply them. Also, check that MBZ
    // bits are not set.
    Option4ClientFqdnImpl::checkFlags(new_flag, true);
    impl_->flags_ = new_flag;
}

std::pair<Option4ClientFqdn::Rcode, Option4ClientFqdn::Rcode>
Option4ClientFqdn::getRcode() const {
    return (std::make_pair(impl_->rcode1_, impl_->rcode2_));
}

void
Option4ClientFqdn::setRcode(const Rcode& rcode) {
    impl_->rcode1_ = rcode;
    impl_->rcode2_ = rcode;
}

void
Option4ClientFqdn::resetFlags() {
    impl_->flags_ = 0;
}

std::string
Option4ClientFqdn::getDomainName() const {
    if (impl_->domain_name_) {
        return (impl_->domain_name_->toText(impl_->domain_name_type_ ==
                                            PARTIAL));
    }
    // If an object holding domain-name is NULL it means that the domain-name
    // is empty.
    return ("");
}

void
Option4ClientFqdn::packDomainName(isc::util::OutputBuffer& buf) const {
    // If domain-name is empty, do nothing.
    if (!impl_->domain_name_) {
        return;
    }

    if (getFlag(FLAG_E)) {
        // Domain name, encoded as a set of labels.
        isc::dns::LabelSequence labels(*impl_->domain_name_);
        if (labels.getDataLength() > 0) {
            size_t read_len = 0;
            const uint8_t* data = labels.getData(&read_len);
            if (impl_->domain_name_type_ == PARTIAL) {
                --read_len;
            }
            buf.writeData(data, read_len);
        }

    } else {
        std::string domain_name = getDomainName();
        if (!domain_name.empty()) {
            buf.writeData(&domain_name[0], domain_name.size());
        }

    }
}

void
Option4ClientFqdn::setDomainName(const std::string& domain_name,
                                 const DomainNameType domain_name_type) {
    impl_->setDomainName(domain_name, domain_name_type);
}

void
Option4ClientFqdn::resetDomainName() {
    setDomainName("", PARTIAL);
}

Option4ClientFqdn::DomainNameType
Option4ClientFqdn::getDomainNameType() const {
    return (impl_->domain_name_type_);
}

void
Option4ClientFqdn::pack(isc::util::OutputBuffer& buf, bool check) const {
    // Header = option code and length.
    packHeader(buf, check);
    // Flags field.
    buf.writeUint8(impl_->flags_);
    // RCODE1 and RCODE2
    buf.writeUint8(impl_->rcode1_.getCode());
    buf.writeUint8(impl_->rcode2_.getCode());
    // Domain name.
    packDomainName(buf);
}

void
Option4ClientFqdn::unpack(OptionBufferConstIter first,
                          OptionBufferConstIter last) {
    setData(first, last);
    impl_->parseWireData(first, last);
    // Check that the flags in the received option are valid. Ignore MBZ bits,
    // because we don't want to discard the whole option because of MBZ bits
    // being set.
    impl_->checkFlags(impl_->flags_, false);
}

std::string
Option4ClientFqdn::toText(int indent) const {
    std::ostringstream stream;
    std::string in(indent, ' '); // base indentation
    stream << in  << "type=" << type_ << " (CLIENT_FQDN), "
           <<  "flags: ("
           << "N=" << (getFlag(FLAG_N) ? "1" : "0") << ", "
           << "E=" << (getFlag(FLAG_E) ? "1" : "0") << ", "
           << "O=" << (getFlag(FLAG_O) ? "1" : "0") << ", "
           << "S=" << (getFlag(FLAG_S) ? "1" : "0") << "), "
           << "domain-name='" << getDomainName() << "' ("
           << (getDomainNameType() == PARTIAL ? "partial" : "full")
           << ")";

    return (stream.str());
}

uint16_t
Option4ClientFqdn::len() const {
    uint16_t domain_name_length = 0;
    // Try to calculate the length of the domain name only if there is
    // any domain name specified.
    if (impl_->domain_name_) {
        // If the E flag is specified, the domain name is encoded in the
        // canonical format. The length of the domain name depends on
        // whether it is a partial or fully qualified names. For the
        // former the length is 1 octet lesser because it lacks terminating
        // zero.
        if (getFlag(FLAG_E)) {
            domain_name_length = impl_->domain_name_type_ == FULL ?
                impl_->domain_name_->getLength() :
                impl_->domain_name_->getLength() - 1;

        // ASCII encoded domain name. Its length is just a length of the
        // string holding the name.
        } else {
            domain_name_length = getDomainName().length();
        }
    }

    return (getHeaderLen() + FIXED_FIELDS_LEN + domain_name_length);
}

} // end of isc::dhcp namespace
} // end of isc namespace
