/******************************************************************************
 *
 * Project:  CPL - Common Portability Library
 * Purpose:  JSon streaming parser
 * Author:   Even Rouault, even.rouault at spatialys.com
 *
 ******************************************************************************
 * Copyright (c) 2017, Even Rouault <even.rouault at spatialys.com>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included
 * in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 ****************************************************************************/

/*! @cond Doxygen_Suppress */

#include <assert.h>
#include <ctype.h> // isdigit...
#include <stdio.h> // snprintf
#include <string.h> // strlen
#include <vector>
#include <string>

#include "cpl_conv.h"
#include "cpl_string.h"
#include "cpl_json_streaming_parser.h"

/************************************************************************/
/*                       CPLJSonStreamingParser()                       */
/************************************************************************/

CPLJSonStreamingParser::CPLJSonStreamingParser()
{
    m_aState.push_back(INIT);
}

/************************************************************************/
/*                      ~CPLJSonStreamingParser()                       */
/************************************************************************/

CPLJSonStreamingParser::~CPLJSonStreamingParser()
{
}

/************************************************************************/
/*                           SetMaxDepth()                              */
/************************************************************************/

void CPLJSonStreamingParser::SetMaxDepth(size_t nVal)
{
    m_nMaxDepth = nVal;
}
/************************************************************************/
/*                         SetMaxStringSize()                           */
/************************************************************************/

void CPLJSonStreamingParser::SetMaxStringSize(size_t nVal)
{
    m_nMaxStringSize = nVal;
}

/************************************************************************/
/*                                Reset()                               */
/************************************************************************/

void CPLJSonStreamingParser::Reset()
{
    m_bExceptionOccurred = false;
    m_bElementFound = false;
    m_nLastChar = 0;
    m_nLineCounter = 1;
    m_nCharCounter = 1;
    m_aState.clear();
    m_aState.push_back(INIT);
    m_osToken.clear();
    m_abArrayState.clear();
    m_aeObjectState.clear();
    m_bInStringEscape = false;
    m_bInUnicode = false;
    m_osUnicodeHex.clear();
}

/************************************************************************/
/*                              AdvanceChar()                           */
/************************************************************************/

void CPLJSonStreamingParser::AdvanceChar(const char*& pStr, size_t& nLength)
{
    if( *pStr == 13 && m_nLastChar != 10 )
    {
        m_nLineCounter ++;
        m_nCharCounter = 0;
    }
    else if( *pStr == 10 && m_nLastChar != 13 )
    {
        m_nLineCounter ++;
        m_nCharCounter = 0;
    }
    m_nLastChar = *pStr;

    pStr ++;
    nLength --;
    m_nCharCounter ++;
}

/************************************************************************/
/*                               SkipSpace()                            */
/************************************************************************/

void CPLJSonStreamingParser::SkipSpace(const char*& pStr, size_t& nLength)
{
    while( nLength > 0 && isspace(*pStr) )
    {
        AdvanceChar(pStr, nLength);
    }
}

/************************************************************************/
/*                             EmitException()                          */
/************************************************************************/

bool CPLJSonStreamingParser::EmitException(const char* pszMessage)
{
    m_bExceptionOccurred = true;
    CPLString osMsg;
    osMsg.Printf("At line %d, character %d: %s",
                 m_nLineCounter, m_nCharCounter, pszMessage);
    Exception(osMsg.c_str());
    return false;
}

/************************************************************************/
/*                          EmitUnexpectedChar()                        */
/************************************************************************/

bool CPLJSonStreamingParser::EmitUnexpectedChar(char ch,
                                                const char* pszExpecting)
{
    char szMessage[64];
    if( pszExpecting )
    {
        snprintf(szMessage, sizeof(szMessage),
             "Unexpected character (%c). Expecting %s", ch, pszExpecting);
    }
    else
    {
        snprintf(szMessage, sizeof(szMessage),
                "Unexpected character (%c)", ch);
    }
    return EmitException(szMessage);
}

/************************************************************************/
/*                            IsValidNewToken()                         */
/************************************************************************/

static bool IsValidNewToken(char ch)
{
    return ch == '[' || ch == '{' || ch == '"' || ch == '-' ||
           ch == '.' || isdigit(ch) || ch == 't' || ch == 'f' || ch == 'n' ||
           ch == 'i' || ch == 'I' || ch == 'N';
}

/************************************************************************/
/*                             StartNewToken()                          */
/************************************************************************/

bool CPLJSonStreamingParser::StartNewToken(const char*& pStr, size_t& nLength)
{
    char ch = *pStr;
    if( ch == '{' )
    {
        if( m_aState.size() == m_nMaxDepth )
        {
            return EmitException("Too many nested objects and/or arrays");
        }
        StartObject();
        m_aeObjectState.push_back(WAITING_KEY);
        m_aState.push_back(OBJECT);
        AdvanceChar(pStr, nLength);
    }
    else if( ch == '"' )
    {
        m_aState.push_back(STRING);
        AdvanceChar(pStr, nLength);
    }
    else if( ch == '[' )
    {
        if( m_aState.size() == m_nMaxDepth )
        {
            return EmitException("Too many nested objects and/or arrays");
        }
        StartArray();
        m_abArrayState.push_back(ArrayState::INIT);
        m_aState.push_back(ARRAY);
        AdvanceChar(pStr, nLength);
    }
    else if( ch == '-' || ch == '.' || isdigit(ch) ||
             ch == 'i' || ch == 'I' || ch == 'N' )
    {
        m_aState.push_back(NUMBER);
    }
    else if( ch == 't' )
    {
        m_aState.push_back(STATE_TRUE);
    }
    else if( ch == 'f' )
    {
        m_aState.push_back(STATE_FALSE);
    }
    else if( ch == 'n' )
    {
        m_aState.push_back(STATE_NULL); /* might be nan */
    }
    else
    {
        assert( false );
    }
    return true;
}

/************************************************************************/
/*                       CheckAndEmitTrueFalseOrNull()                  */
/************************************************************************/

bool CPLJSonStreamingParser::CheckAndEmitTrueFalseOrNull(char ch)
{
    State eCurState = currentState();

    if( eCurState == STATE_TRUE )
    {
        if( m_osToken == "true" )
        {
            Boolean(true);
        }
        else
        {
            return EmitUnexpectedChar(ch);
        }
    }
    else if( eCurState == STATE_FALSE)
    {
        if( m_osToken == "false" )
        {
            Boolean(false);
        }
        else
        {
            return EmitUnexpectedChar(ch);
        }
    }
    else /* if( eCurState == STATE_NULL ) */
    {
        if( m_osToken == "null" )
        {
            Null();
        }
        else
        {
            return EmitUnexpectedChar(ch);
        }
    }
    m_aState.pop_back();
    m_osToken.clear();
    return true;
}

/************************************************************************/
/*                           CheckStackEmpty()                          */
/************************************************************************/

bool CPLJSonStreamingParser::CheckStackEmpty()
{
    if( !m_aeObjectState.empty() )
    {
        return EmitException("Unterminated object");
    }
    else if( !m_abArrayState.empty() )
    {
        return EmitException("Unterminated array");
    }
    return true;
}

/************************************************************************/
/*                           IsHighSurrogate()                          */
/************************************************************************/

static bool IsHighSurrogate(unsigned uc)
{
    return (uc & 0xFC00) == 0xD800;
}

/************************************************************************/
/*                           IsLowSurrogate()                           */
/************************************************************************/

static bool IsLowSurrogate(unsigned uc)
{
    return (uc & 0xFC00) == 0xDC00;
}

/************************************************************************/
/*                         GetSurrogatePair()                           */
/************************************************************************/

static unsigned GetSurrogatePair(unsigned hi, unsigned lo)
{
    return ((hi & 0x3FF) << 10) + (lo & 0x3FF) + 0x10000;
}

/************************************************************************/
/*                            IsHexDigit()                              */
/************************************************************************/

static bool IsHexDigit(char ch)
{
    return (ch >= '0' && ch <= '9') ||
           (ch >= 'a' && ch <= 'f') ||
           (ch >= 'A' && ch <= 'F');
}

/************************************************************************/
/*                           HexToDecimal()                             */
/************************************************************************/

static unsigned HexToDecimal(char ch)
{
    if( ch >= '0' && ch <= '9' )
        return ch - '0';
    if (ch >= 'a' && ch <= 'f' )
        return 10 + ch - 'a';
    //if (ch >= 'A' && ch <= 'F' )
    return 10 + ch - 'A';
}

/************************************************************************/
/*                            getUCSChar()                              */
/************************************************************************/

static unsigned getUCSChar(const std::string& unicode4HexChar)
{
    return (HexToDecimal(unicode4HexChar[0]) << 12) |
           (HexToDecimal(unicode4HexChar[1]) << 8) |
           (HexToDecimal(unicode4HexChar[2]) << 4) |
           (HexToDecimal(unicode4HexChar[3]));
}

/************************************************************************/
/*                           DecodeUnicode()                            */
/************************************************************************/

void CPLJSonStreamingParser::DecodeUnicode()
{
    constexpr char szReplacementUTF8[] = "\xEF\xBF\xBD";
    unsigned nUCSChar;
    if( m_osUnicodeHex.size() == 8 )
    {
        unsigned nUCSHigh = getUCSChar(m_osUnicodeHex);
        assert( IsHighSurrogate(nUCSHigh) );
        unsigned nUCSLow = getUCSChar(m_osUnicodeHex.substr(4));
        if( IsLowSurrogate(nUCSLow) )
        {
            nUCSChar = GetSurrogatePair(nUCSHigh, nUCSLow);
        }
        else
        {
            /* Invalid code point. Insert the replacement char */
            nUCSChar = 0xFFFFFFFFU;
        }
    }
    else
    {
        assert( m_osUnicodeHex.size() == 4 );
        nUCSChar = getUCSChar(m_osUnicodeHex);
    }

    if( nUCSChar < 0x80)
    {
        m_osToken += static_cast<char>(nUCSChar);
    }
    else if( nUCSChar < 0x800)
    {
        m_osToken += static_cast<char>(0xC0 | (nUCSChar >> 6));
        m_osToken += static_cast<char>(0x80 | (nUCSChar & 0x3F));
    }
    else if (IsLowSurrogate(nUCSChar) ||
             IsHighSurrogate(nUCSChar) )
    {
        /* Invalid code point. Insert the replacement char */
        m_osToken += szReplacementUTF8;
    }
    else if (nUCSChar < 0x10000)
    {
        m_osToken += static_cast<char>(0xE0 | (nUCSChar >> 12));
        m_osToken += static_cast<char>(0x80 | ((nUCSChar >> 6) & 0x3F));
        m_osToken += static_cast<char>(0x80 | (nUCSChar & 0x3F));
    }
    else if (nUCSChar < 0x110000)
    {
        m_osToken += static_cast<char>(0xF0 | ((nUCSChar >> 18) & 0x07));
        m_osToken += static_cast<char>(0x80 | ((nUCSChar >> 12) & 0x3F));
        m_osToken += static_cast<char>(0x80 | ((nUCSChar >> 6) & 0x3F));
        m_osToken += static_cast<char>(0x80 | (nUCSChar & 0x3F));
    }
    else
    {
        /* Invalid code point. Insert the replacement char */
        m_osToken += szReplacementUTF8;
    }

    m_bInUnicode = false;
    m_osUnicodeHex.clear();
}

/************************************************************************/
/*                              Parse()                                 */
/************************************************************************/

bool CPLJSonStreamingParser::Parse(const char* pStr, size_t nLength,
                                   bool bFinished)
{
    if( m_bExceptionOccurred )
        return false;

    while( true )
    {
        State eCurState = currentState();
        if( eCurState == INIT )
        {
            SkipSpace(pStr, nLength);
            if( nLength == 0 )
                return true;
            if( m_bElementFound || !IsValidNewToken(*pStr) )
            {
                return EmitUnexpectedChar(*pStr);
            }
            if( !StartNewToken(pStr, nLength) )
            {
                return false;
            }
            m_bElementFound = true;
        }
        else if( eCurState == NUMBER )
        {
            while(nLength)
            {
                char ch = *pStr;
                if( ch == '+' || ch == '-' || isdigit(ch) ||
                    ch == '.' || ch == 'e' || ch == 'E' )
                {
                    if( m_osToken.size() == 1024 )
                    {
                        return EmitException("Too many characters in number");
                    }
                    m_osToken += ch;
                }
                else if( isspace(ch) || ch == ',' || ch == '}' || ch == ']' )
                {
                    SkipSpace(pStr, nLength);
                    break;
                }
                else
                {
                    CPLString extendedToken(m_osToken + ch);
                    if( (STARTS_WITH_CI("Infinity", extendedToken) &&
                          m_osToken.size() + 1 <= strlen("Infinity")) ||
                         (STARTS_WITH_CI("-Infinity", extendedToken) &&
                          m_osToken.size() + 1 <= strlen("-Infinity")) ||
                         (STARTS_WITH_CI("NaN", extendedToken) &&
                          m_osToken.size() + 1 <= strlen("NaN")) )
                    {
                        m_osToken += ch;
                    }
                    else
                    {
                        return EmitUnexpectedChar(ch);
                    }
                }
                AdvanceChar(pStr, nLength);
            }

            if( nLength != 0 || bFinished )
            {
                const char firstCh = m_osToken[0];
                if( firstCh == 'i' || firstCh == 'I' )
                {
                    if( !EQUAL(m_osToken.c_str(), "Infinity") )
                    {
                        return EmitException("Invalid number");
                    }
                }
                else if( firstCh == '-' )
                {
                    if( m_osToken[1] == 'i' || m_osToken[1] == 'I' )
                    {
                        if( !EQUAL(m_osToken.c_str(), "-Infinity") )
                        {
                            return EmitException("Invalid number");
                        }
                    }
                }
                else if( firstCh == 'n' || firstCh == 'N' )
                {
                    if( m_osToken[1] == 'a' || m_osToken[1] == 'A' )
                    {
                        if( !EQUAL(m_osToken.c_str(), "NaN") )
                        {
                            return EmitException("Invalid number");
                        }
                    }
                }

                Number(m_osToken.c_str(), m_osToken.size());
                m_osToken.clear();
                m_aState.pop_back();
            }

            if( nLength == 0 )
            {
                if( bFinished )
                {
                    return CheckStackEmpty();
                }
                return true;
            }
        }
        else if( eCurState == STRING )
        {
            bool bEOS = false;
            while( nLength )
            {
                if( m_osToken.size() == m_nMaxStringSize )
                {
                    return EmitException("Too many characters in number");
                }

                char ch = *pStr;
                if( m_bInUnicode)
                {
                    if( m_osUnicodeHex.size() == 8 )
                    {
                        DecodeUnicode();
                    }
                    else if( m_osUnicodeHex.size() == 4 )
                    {
                        /* Start of next surrogate pair ? */
                        if( m_nLastChar == '\\' )
                        {
                            if( ch == 'u' )
                            {
                                AdvanceChar(pStr, nLength);
                                continue;
                            }
                            else
                            {
                                /* will be replacement character */
                                DecodeUnicode();
                                m_bInStringEscape = true;
                            }
                        }
                        else if( m_nLastChar == 'u' )
                        {
                            if( IsHexDigit(ch) )
                            {
                                m_osUnicodeHex += ch;
                            }
                            else
                            {
                                char szMessage[64];
                                snprintf(szMessage, sizeof(szMessage),
                                    "Illegal character in unicode "
                                    "sequence (\\%c)", ch);
                                return EmitException(szMessage);
                            }
                            AdvanceChar(pStr, nLength);
                            continue;
                        }
                        else if( ch == '\\' )
                        {
                            AdvanceChar(pStr, nLength);
                            continue;
                        }
                        else
                        {
                            /* will be replacement character */
                            DecodeUnicode();
                        }
                    }
                    else
                    {
                        if( IsHexDigit(ch) )
                        {
                            m_osUnicodeHex += ch;
                            if( m_osUnicodeHex.size() == 4 &&
                                !IsHighSurrogate(getUCSChar(m_osUnicodeHex)) )
                            {
                                DecodeUnicode();
                            }
                        }
                        else
                        {
                            char szMessage[64];
                            snprintf(szMessage, sizeof(szMessage),
                                "Illegal character in unicode "
                                "sequence (\\%c)", ch);
                            return EmitException(szMessage);
                        }
                        AdvanceChar(pStr, nLength);
                        continue;
                    }
                }

                if( m_bInStringEscape )
                {
                    if( ch == '"' || ch == '\\' || ch == '/' )
                        m_osToken += ch;
                    else if( ch == 'b' )
                        m_osToken += '\b';
                    else if( ch == 'f' )
                        m_osToken += '\f';
                    else if( ch == 'n' )
                        m_osToken += '\n';
                    else if( ch == 'r' )
                        m_osToken += '\r';
                    else if( ch == 't' )
                        m_osToken += '\t';
                    else if( ch == 'u' )
                    {
                        m_bInUnicode = true;
                    }
                    else
                    {
                        char szMessage[32];
                        snprintf(szMessage, sizeof(szMessage),
                                 "Illegal escape sequence (\\%c)", ch);
                        return EmitException(szMessage);
                    }
                    m_bInStringEscape = false;
                    AdvanceChar(pStr, nLength);
                    continue;
                }
                else if( ch == '\\' )
                {
                    m_bInStringEscape = true;
                    AdvanceChar(pStr, nLength);
                    continue;
                }
                else if( ch == '"' )
                {
                    bEOS = true;
                    AdvanceChar(pStr, nLength);
                    SkipSpace(pStr, nLength);

                    if( !m_aeObjectState.empty() &&
                        m_aeObjectState.back() == IN_KEY )
                    {
                        StartObjectMember(m_osToken.c_str(), m_osToken.size());
                    }
                    else
                    {
                        String(m_osToken.c_str(), m_osToken.size());
                    }
                    m_osToken.clear();
                    m_aState.pop_back();

                    break;
                }

                m_osToken += ch;
                AdvanceChar(pStr, nLength);
            }

            if( nLength == 0 )
            {
                if( bFinished )
                {
                    if( !bEOS )
                    {
                        return EmitException("Unterminated string");
                    }
                    return CheckStackEmpty();
                }
                return true;
            }
        }
        else if( eCurState == ARRAY )
        {
            SkipSpace(pStr, nLength);
            if( nLength == 0 )
            {
                if( bFinished )
                {
                    return EmitException("Unterminated array");
                }
                return true;
            }

            char ch = *pStr;
            if( ch == ',' )
            {
                if( m_abArrayState.back() != ArrayState::AFTER_VALUE )
                {
                    return EmitUnexpectedChar(ch, "','");
                }
                m_abArrayState.back() = ArrayState::AFTER_COMMA;
                AdvanceChar(pStr, nLength);
            }
            else if( ch == ']' )
            {
                if( m_abArrayState.back() == ArrayState::AFTER_COMMA)
                {
                    return EmitException("Missing value");
                }

                EndArray();
                AdvanceChar(pStr, nLength);
                m_abArrayState.pop_back();
                m_aState.pop_back();
            }
            else if( IsValidNewToken(ch) )
            {
                if( m_abArrayState.back() == ArrayState::AFTER_VALUE )
                {
                    return EmitException("Unexpected state: ',' or ']' expected");
                }
                m_abArrayState.back() = ArrayState::AFTER_VALUE;

                StartArrayMember();
                if( !StartNewToken(pStr, nLength) )
                {
                    return false;
                }
            }
            else
            {
                return EmitUnexpectedChar(ch);
            }
        }
        else if( eCurState == OBJECT )
        {
            SkipSpace(pStr, nLength);
            if( nLength == 0 )
            {
                if( bFinished )
                {
                    return EmitException("Unterminated object");
                }
                return true;
            }

            char ch = *pStr;
            if( ch == ',' )
            {
                if( m_aeObjectState.back() != IN_VALUE )
                {
                    return EmitUnexpectedChar(ch, "','");
                }

                m_aeObjectState.back() = WAITING_KEY;
                AdvanceChar(pStr, nLength);
            }
            else if( ch == ':' )
            {
                if( m_aeObjectState.back() != IN_KEY )
                {
                    return EmitUnexpectedChar(ch, "':'");
                }
                m_aeObjectState.back() = KEY_FINISHED;
                AdvanceChar(pStr, nLength);
            }
            else if( ch == '}' )
            {
                if( m_aeObjectState.back() == WAITING_KEY ||
                    m_aeObjectState.back() == IN_VALUE )
                {
                    // nothing
                }
                else
                {
                    return EmitException("Missing value");
                }

                EndObject();
                AdvanceChar(pStr, nLength);
                m_aeObjectState.pop_back();
                m_aState.pop_back();
            }
            else if( IsValidNewToken(ch) )
            {
                if( m_aeObjectState.back() == WAITING_KEY )
                {
                    if( ch != '"' )
                    {
                        return EmitUnexpectedChar(ch, "'\"'");
                    }
                     m_aeObjectState.back() = IN_KEY;
                }
                else if( m_aeObjectState.back() == KEY_FINISHED )
                {
                    m_aeObjectState.back() = IN_VALUE;
                }
                else
                {
                    return EmitException("Unexpected state");
                }
                if( !StartNewToken(pStr, nLength) )
                {
                    return false;
                }
            }
            else
            {
                return EmitUnexpectedChar(ch);
            }
        }
        else /* if( eCurState == STATE_TRUE || eCurState == STATE_FALSE ||
                    eCurState == STATE_NULL ) */
        {
            while(nLength)
            {
                char ch = *pStr;
                if( eCurState == STATE_NULL && (ch == 'a' || ch == 'A') &&
                    m_osToken.size() == 1 )
                {
                    m_aState.back() = NUMBER;
                    break;
                }
                if( isalpha(ch) )
                {
                    m_osToken += ch;
                    if( eCurState == STATE_TRUE &&
                        (m_osToken.size() > strlen("true") ||
                        memcmp(m_osToken.c_str(), "true",
                               m_osToken.size()) != 0) )
                    {
                        return EmitUnexpectedChar(*pStr);
                    }
                    else if( eCurState == STATE_FALSE &&
                        (m_osToken.size() > strlen("false") ||
                        memcmp(m_osToken.c_str(), "false",
                               m_osToken.size()) != 0) )
                    {
                        return EmitUnexpectedChar(*pStr);
                    }
                    else if( eCurState == STATE_NULL &&
                        (m_osToken.size() > strlen("null") ||
                        memcmp(m_osToken.c_str(), "null",
                               m_osToken.size()) != 0) )
                    {
                        return EmitUnexpectedChar(*pStr);
                    }
                }
                else if( isspace(ch) || ch == ',' || ch == '}' || ch == ']' )
                {
                    SkipSpace(pStr, nLength);
                    break;
                }
                else
                {
                    return EmitUnexpectedChar(ch);
                }
                AdvanceChar(pStr, nLength);
            }
            if( m_aState.back() == NUMBER )
            {
                continue;
            }
            if( nLength == 0 )
            {
                if( bFinished )
                {
                    if( !CheckAndEmitTrueFalseOrNull(0) )
                        return false;
                    return CheckStackEmpty();
                }
                return true;
            }

            if( !CheckAndEmitTrueFalseOrNull(*pStr) )
                return false;
        }
    }
}


/************************************************************************/
/*                       GetSerializedString()                          */
/************************************************************************/

std::string CPLJSonStreamingParser::GetSerializedString(const char* pszStr)
{
    std::string osStr("\"");
    for( int i = 0; pszStr[i]; i++ )
    {
        char ch = pszStr[i];
        if( ch == '\b' )
            osStr += "\\b";
        else if( ch == '\f' )
            osStr += "\\f";
        else if( ch == '\n' )
            osStr += "\\n";
        else if( ch == '\r' )
            osStr += "\\r";
        else if( ch == '\t' )
            osStr += "\\t";
        else if( ch == '"' )
            osStr += "\\\"";
        else if( ch == '\\' )
            osStr += "\\\\";
        else if( static_cast<unsigned char>(ch) < ' ' )
            osStr += CPLSPrintf("\\u%04X", ch);
        else
            osStr += ch;
    }
    osStr += "\"";
    return osStr;
}

/*! @endcond */
