// $OpenLDAP$
/*
 * Copyright 2008-2021 The OpenLDAP Foundation, All Rights Reserved.
 * COPYING RESTRICTIONS APPLY, see COPYRIGHT file
 */

#include "LdifReader.h"
#include "LDAPMessage.h"
#include "LDAPEntry.h"
#include "LDAPAttributeList.h"
#include "LDAPAttribute.h"
#include "LDAPUrl.h"
#include "debug.h"

#include <string>
#include <sstream>
#include <stdexcept>

#include <sasl/saslutil.h> // For base64 routines

typedef std::pair<std::string, std::string> stringpair;

LdifReader::LdifReader( std::istream &input ) 
        : m_ldifstream(input), m_lineNumber(0)
{
    DEBUG(LDAP_DEBUG_TRACE, "<> LdifReader::LdifReader()" << std::endl);
    this->m_version = 0;
    // read the first record to find out version and type of the LDIF
    this->readNextRecord(true);
    this->m_currentIsFirst = true;
}

int LdifReader::readNextRecord( bool first )
{
    DEBUG(LDAP_DEBUG_TRACE, "-> LdifReader::readRecord()" << std::endl);
    std::string line;
    std::string type;
    std::string value;
    int numLine = 0;
    int recordType = 0;

    if ( (! first) && this->m_currentIsFirst == true )
    {
        this->m_currentIsFirst = false;
        return m_curRecType;
    }

    m_currentRecord.clear();

    while ( !this->getLdifLine(line) )
    {
        DEBUG(LDAP_DEBUG_TRACE, "  Line: " << line << std::endl );

        // skip comments and empty lines between entries
        if ( line[0] == '#' || ( numLine == 0 && line.size() == 0 ) )
        {
            DEBUG(LDAP_DEBUG_TRACE, "skipping empty line or comment" << std::endl );
            continue;
        }
        if ( line.size() == 0 ) 
        {
            // End of Entry
            break;
        }

        this->splitLine(line, type, value);

        if ( numLine == 0 )
        {
            if ( type == "version" )
            {
                std::istringstream valuestream(value);
                valuestream >> this->m_version;
                if ( this->m_version != 1 ) // there is no other Version than LDIFv1 
                {
                    std::ostringstream err;
                    err << "Line " << this->m_lineNumber 
                        << ": Unsupported LDIF Version";
                    throw( std::runtime_error(err.str()) );
                }
                continue;
            }
            if ( type == "dn" ) // Record should start with the DN ...
            {
                DEBUG(LDAP_DEBUG_TRACE, " Record DN:" << value << std::endl);
            }
            else if ( type == "include" ) // ... or it might be an "include" line
            {
                DEBUG(LDAP_DEBUG_TRACE, " Include directive: " << value << std::endl);
                if ( this->m_version == 1 )
                {
                    std::ostringstream err;
                    err << "Line " << this->m_lineNumber 
                        << ": \"include\" not allowed in LDIF version 1.";
                    throw( std::runtime_error(err.str()) );
                }
                else
                {
                    std::ostringstream err;
                    err << "Line " << this->m_lineNumber 
                        << ": \"include\" not yet supported.";
                    throw( std::runtime_error(err.str()) );
                }
            }
            else
            {
                DEBUG(LDAP_DEBUG_TRACE, " Record doesn't start with a DN" 
                            << std::endl);
                std::ostringstream err;
                err << "Line " << this->m_lineNumber 
                    << ": LDIF record does not start with a DN.";
                throw( std::runtime_error(err.str()) );
            }
        }
        if ( numLine == 1 ) // might contain "changtype" to indicate a change request
        {
            if ( type == "changetype" ) 
            {
                if ( first ) 
                {
                    this->m_ldifTypeRequest = true;
                }
                else if (! this->m_ldifTypeRequest )
                {
                    // Change Request in Entry record LDIF, should we accept it?
                    std::ostringstream err;
                    err << "Line " << this->m_lineNumber 
                        << ": Change Request in an entry-only LDIF.";
                    throw( std::runtime_error(err.str()) );
                }
                if ( value == "modify" )
                {
                    recordType = LDAPMsg::MODIFY_REQUEST;
                }
                else if ( value == "add" )
                {
                    recordType = LDAPMsg::ADD_REQUEST;
                }
                else if ( value == "delete" )
                {
                    recordType = LDAPMsg::DELETE_REQUEST;
                }
                else if ( value == "modrdn" )
                {   
                    recordType = LDAPMsg::MODRDN_REQUEST;
                }
                else
                {
                    DEBUG(LDAP_DEBUG_TRACE, " Unknown change request <" 
                            << value << ">" << std::endl);
                    std::ostringstream err;
                    err << "Line " << this->m_lineNumber 
                        << ": Unknown changetype: \"" << value << "\".";
                    throw( std::runtime_error(err.str()) );
                }
            }
            else
            {
                if ( first ) 
                {
                    this->m_ldifTypeRequest = false;
                }
                else if (this->m_ldifTypeRequest )
                {
                    // Entry record in Change record LDIF, should we accept 
                    // it (e.g. as AddRequest)?
                }
                recordType = LDAPMsg::SEARCH_ENTRY;
            }
        }
        m_currentRecord.push_back( stringpair(type, value) );
        numLine++;
    }
    DEBUG(LDAP_DEBUG_TRACE, "<- LdifReader::readRecord() return: " 
            << recordType << std::endl);
    m_curRecType = recordType;
    return recordType;
}

LDAPEntry LdifReader::getEntryRecord()
{
    std::list<stringpair>::const_iterator i = m_currentRecord.begin();
    if ( m_curRecType != LDAPMsg::SEARCH_ENTRY )
    {
        throw( std::runtime_error( "The LDIF record: '" + i->second +
                                   "' is not a valid LDAP Entry" ));
    }
    LDAPEntry resEntry(i->second);
    i++;
    LDAPAttribute curAttr(i->first);
    LDAPAttributeList *curAl = new LDAPAttributeList();
    for ( ; i != m_currentRecord.end(); i++ )
    {
        if ( i->first == curAttr.getName() )
        {
            curAttr.addValue(i->second);
        }
        else
        {
            const LDAPAttribute* existing = curAl->getAttributeByName( i->first );
            if ( existing )
            {
                // Attribute exists already (handle gracefully)
                curAl->addAttribute( curAttr );
                curAttr = LDAPAttribute( *existing );
                curAttr.addValue(i->second);
                curAl->delAttribute( i->first );
            }
            else
            {
                curAl->addAttribute( curAttr );
                curAttr = LDAPAttribute( i->first, i->second );
            }
        }
    }
    curAl->addAttribute( curAttr );
    resEntry.setAttributes( curAl );
    return resEntry;
}

int LdifReader::getLdifLine(std::string &ldifline)
{
    DEBUG(LDAP_DEBUG_TRACE, "-> LdifReader::getLdifLine()" << std::endl);

    this->m_lineNumber++;
    if ( ! getline(m_ldifstream, ldifline) )
    {
        return -1;
    }
    while ( m_ldifstream &&
        (m_ldifstream.peek() == ' ' || m_ldifstream.peek() == '\t'))
    {
        std::string cat;
        m_ldifstream.ignore();
        getline(m_ldifstream, cat);
        ldifline += cat;
        this->m_lineNumber++;
    }

    DEBUG(LDAP_DEBUG_TRACE, "<- LdifReader::getLdifLine()" << std::endl);
    return 0;
}

void LdifReader::splitLine(
            const std::string& line, 
            std::string &type,
            std::string &value) const
{
    std::string::size_type pos = line.find(':');
    if ( pos == std::string::npos )
    {
        DEBUG(LDAP_DEBUG_ANY, "Invalid LDIF line. No `:` separator" 
                << std::endl );
        std::ostringstream err;
        err << "Line " << this->m_lineNumber << ": Invalid LDIF line. No `:` separator";
        throw( std::runtime_error( err.str() ));
    }

    type = line.substr(0, pos);
    if ( pos == line.size() )
    {
        // empty value
        value = "";
        return;
    }

    pos++;
    char delim = line[pos];
    if ( delim == ':' || delim == '<' )
    {
        pos++;
    }

    for( ; pos < line.size() && isspace(line[pos]); pos++ )
    { /* empty */ }

    value = line.substr(pos);

    if ( delim == ':' )
    {
        // Base64 encoded value
        DEBUG(LDAP_DEBUG_TRACE, "  base64 encoded value" << std::endl );
        char outbuf[value.size()];
        int rc = sasl_decode64(value.c_str(), value.size(), 
                outbuf, value.size(), NULL);
        if( rc == SASL_OK )
        {
            value = std::string(outbuf);
        }
        else if ( rc == SASL_BADPROT )
        {
            value = "";
            DEBUG( LDAP_DEBUG_TRACE, " invalid base64 content" << std::endl );
            std::ostringstream err;
            err << "Line " << this->m_lineNumber << ": Can't decode Base64 data";
            throw( std::runtime_error( err.str() ));
        }
        else if ( rc == SASL_BUFOVER )
        {
            value = "";
            DEBUG( LDAP_DEBUG_TRACE, " not enough space in output buffer" 
                    << std::endl );
            std::ostringstream err;
            err << "Line " << this->m_lineNumber 
                << ": Can't decode Base64 data. Buffer too small";
            throw( std::runtime_error( err.str() ));
        }
    }
    else if ( delim == '<' )
    {
        // URL value
        DEBUG(LDAP_DEBUG_TRACE, "  url value" << std::endl );
        std::ostringstream err;
        err << "Line " << this->m_lineNumber 
            << ": URLs are currently not supported";
        throw( std::runtime_error( err.str() ));
    }
    else 
    {
        // "normal" value
        DEBUG(LDAP_DEBUG_TRACE, "  string value" << std::endl );
    }
    DEBUG(LDAP_DEBUG_TRACE, "  Type: <" << type << ">" << std::endl );
    DEBUG(LDAP_DEBUG_TRACE, "  Value: <" << value << ">" << std::endl );
    return;
}

std::string LdifReader::readIncludeLine( const std::string& line ) const
{
    std::string::size_type pos = sizeof("file:") - 1;
    std::string scheme = line.substr( 0, pos );
    std::string file;

    // only file:// URLs supported currently
    if ( scheme != "file:" )
    {
        DEBUG( LDAP_DEBUG_TRACE, "unsupported scheme: " << scheme 
                << std::endl);
    }
    else if ( line[pos] == '/' )
    {
        if ( line[pos+1] == '/' )
        {
            pos += 2;
        }
        file = line.substr(pos, std::string::npos);
        DEBUG( LDAP_DEBUG_TRACE, "target file: " << file << std::endl);
    }
    return file;
}
