//+---------------------------------------------------------------------------
//
//  File:       inistr.cpp
//
//  Contents:   SHGet/SetIniStringW implementations, which save strings into
//              INI files in a manner that survives the round trip to disk.
//
//----------------------------------------------------------------------------

#include "priv.h"
#define _SHELL32_
#define _SHDOCVW_
#include "unicwrap.h"

#include <platform.h>
#include <mlang.h>

//
//  Do this in every wrapper function that has an output parameter.
//  It raises assertion failures on the main code path so that
//  the same assertions are raised on NT and 95.  The CStrOut class
//  doesn't like it when you say that an output buffer is NULL yet
//  has nonzero length.  Without this macro, the bug would go undetected
//  on NT and appear only on Win95.
//
#define VALIDATE_OUTBUF(s, cch) ASSERT((s) != NULL || (cch) == 0)

//----------------------------------------------------------------------------
//
//  The basic problem is that INI files are ANSI-only, so any UNICODE
//  string you put into it won't round-trip.
//
//  So the solution is to record UNICODE strings in UTF7.  Why UTF7?
//  Because we can't use UTF8, since XxxPrivateProfileStringW will try
//  to convert the 8-bit values to/from UNICODE and mess them up.  Since
//  some of the 8-bit values might not even be valid (e.g., a DBCS lead
//  byte followed by an illegal trail byte), we cannot assume that the
//  string will survive the ANSI -> UNICODE -> ANSI round-trip.
//
//  The UTF7 string is stored in a [Section.W] section, under the
//  same key name.  The original ANSI string is stored in a [Section.A]
//  section, again, with the same key name.
//
//  (We separate the A/W from the section name with a dot so it is less
//  likely that we will accidentally collide with other section names.
//
//  We store the original ANSI string twice so we can compare the two
//  and see if a downlevel app (e.g., IE4) has changed the [Section]
//  version.  If so, then we ignore the [Section.W] version since it's stale.
//
//  If the original string is already 7-bit clean, then no UTF7 string is
//  recorded.
//

BOOL
Is7BitClean(LPCWSTR pwsz)
{
    for ( ; *pwsz; pwsz++) {
        if ((UINT)*pwsz > 127)
            return FALSE;
    }
    return TRUE;
}

//----------------------------------------------------------------------------
//
//  Yet another conversion class -- this one is for creating the
//  variants of a section name.
//
//  Note!  Since INI files are ASCII, section names are necessarily 7-bit
//  clean, so we can cheat a lot of stuff.
//

class CStrSectionX : public CConvertStrW
{
public:
    CStrSectionX(LPCWSTR pwszSection);
};

//
//  We append a dot an an A or W to the section name.
//
#define SECTION_SUFFIX_LEN  2

CStrSectionX::CStrSectionX(LPCWSTR pwszSection)
{
    ASSERT(_pwstr == NULL);
    if (pwszSection) {

        ASSERT(Is7BitClean(pwszSection));

        UINT cwchNeeded = lstrlenW(pwszSection) + SECTION_SUFFIX_LEN + 1;
        if (cwchNeeded > ARRAYSIZE(_awch)) {
            _pwstr = new WCHAR[cwchNeeded];
        } else {
            _pwstr = _awch;
        }

        if (_pwstr) {
            // Build the string initially with ".A" stuck on the end
            // It will later get changed to a ".W"
            lstrcpyW(_pwstr, pwszSection);
            lstrcatW(_pwstr, L".A");
        }
    }
}

//----------------------------------------------------------------------------
//
//  Mini-class for keeping track of UTF7 strings.  These are kept in ANSI
//  most of the time since that's what ConvertINetUnicodeToMultiByte uses.
//
//  The UTF7 shadow is prefixed by a checksum of the original string, which
//  we use on read-back to see if the shadow still corresponds to the
//  original string.
//

class CStrUTF7 : public CConvertStr
{
public:
    inline CStrUTF7() : CConvertStr(CP_ACP) { };
    void SetUnicode(LPCWSTR pwszValue);
};

//
//  Note that this can be slow since it happens only when we encounter
//  a non-ANSI character.
//
void CStrUTF7::SetUnicode(LPCWSTR pwszValue)
{
    int cwchLen = lstrlenW(pwszValue);
    HRESULT hres;
    DWORD dwMode;

    int cwchLenT = cwchLen;

    // Save room for terminating NULL.  We must convert the NULL separately
    // because UTF7 does not translate NULL to NULL.
    int cchNeeded = ARRAYSIZE(_ach) - 1;
    dwMode = 0;
    hres = ConvertINetUnicodeToMultiByte(&dwMode, CP_UTF7, pwszValue,
                                         &cwchLenT, _ach,
                                         &cchNeeded);
    if (SUCCEEDED(hres)) {
        ASSERT(cchNeeded + 1 <= ARRAYSIZE(_ach));
        _pstr = _ach;
    } else {
        _pstr = new CHAR[cchNeeded + 1];
        if (!_pstr)
            return;                 // No string - tough

        cwchLenT = cwchLen;
        dwMode = 0;
        hres = ConvertINetUnicodeToMultiByte(&dwMode, CP_UTF7, pwszValue,
                                    &cwchLenT, _pstr,
                                    &cchNeeded);
        if (FAILED(hres)) {         // Couldn't convert - tough
            Free();
            return;
        }
    }

    // Terminate explicitly since UTF7 doesn't.
    _pstr[cchNeeded] = '\0';
}

//
//  pwszSection  = section name into which to write pwszValue (UNICODE)
//  pwszSectionA = section name into which to write ANSI shadow
//  pwszKey      = key name for both pwszValue and strUTF7
//  pwszFileName = file name
//
//  pwszSectionA can be NULL if a low-memory condition was encountered.
//
//  strUTF7 can be NULL, meaning that the shadows should be deleted.
//
//  Write pwszSection first, followed by pwszSectionA, then pwszSectionW.
//  This ensures that the backwards-compatibility string comes first in
//  the file, in case there are apps that assume such.
//
//  pwszSectionW is computed from pwszSectionA by changing the last "A"
//  to a "W".  pwszSecionW gets the UTF7-encoded unicode string.
//  strUTF7 might be NULL, meaning that we should delete the shadow strings.
//
BOOL WritePrivateProfileStringMultiW(LPCWSTR pwszSection,  LPCWSTR pwszValue,
                                      LPWSTR pwszSectionA, CStrUTF7& strUTF7,
                                     LPCWSTR pwszKey,      LPCWSTR pwszFileName)
{
    BOOL fRc = WritePrivateProfileStringW(pwszSection, pwszKey, pwszValue, pwszFileName);

    if (pwszSectionA) {
        //
        //  Write the [Section.A] key, or delete it if there is no UTF7.
        //
        WritePrivateProfileStringW(pwszSectionA, pwszKey,
                                   strUTF7 ? pwszValue : NULL, pwszFileName);

        //
        //  Now change pwszSectionA to pwszSectionW so we can write out
        //  the UTF7 encoding.
        //
        pwszSectionA[lstrlenW(pwszSectionA) - 1] = TEXT('W');

        CStrInW strUTF7W(strUTF7);
        // This really writes [Section.W]
        WritePrivateProfileStringW(pwszSectionA, pwszKey, strUTF7W, pwszFileName);
    }

    return fRc;
}

BOOL WritePrivateProfileStringMultiA(LPCWSTR pwszSection,  LPCWSTR pwszValue,
                                      LPWSTR pwszSectionA, CStrUTF7& strUTF7,
                                     LPCWSTR pwszKey,      LPCWSTR pwszFileName)
{
    CStrIn strSection(pwszSection);
    CStrIn strSectionA(pwszSectionA);
    CStrIn strValue(pwszValue);
    CStrIn strKey(pwszKey);
    CPPFIn strFileName(pwszFileName); // PrivateProfile filename needs special class

    BOOL fRc = WritePrivateProfileStringA(strSection, strKey, strValue, strFileName);

    if ((LPSTR)strSectionA) {
        //
        //  Write the [Section.A] key, or delete it if there is no UTF7.
        //
        WritePrivateProfileStringA(strSectionA, strKey,
#if defined(UNIX) && !defined(ux10)
                                   strUTF7 ? (LPSTR)strValue : (LPSTR)NULL, strFileName);
#else
                                   strUTF7 ? strValue : NULL, strFileName);
#endif

        //
        //  Now change strSectionA to strSectionW so we can write out
        //  the UTF7 encoding.
        //
        strSectionA[lstrlenA(strSectionA) - 1] = 'W';

        // This really writes [Section.W]
        WritePrivateProfileStringA(strSectionA, strKey, strUTF7, strFileName);
    }

    return fRc;
}

BOOL WINAPI
SHSetIniStringW(LPCWSTR pwszSection, LPCWSTR pwszKey, LPCWSTR pwszValue, LPCWSTR pwszFileName)
{
    // We have no way of encoding these two, so they had better by 7-bit clean
    // We also do not support "delete entire section"
    AssertMsg(pwszSection != NULL,
              TEXT("SHSetIniStringW: Section name cannot be NULL; bug in caller"));
    AssertMsg(Is7BitClean(pwszSection),
              TEXT("SHSetIniStringW: Section name not 7-bit clean; bug in caller"));
    AssertMsg(pwszKey != NULL,
              TEXT("SHSetIniStringW: Key name cannot be NULL; bug in caller"));
    AssertMsg(!pwszKey     || Is7BitClean(pwszKey),
              TEXT("SHSetIniStringW: Key name not 7-bit clean; bug in caller"));

    CStrSectionX strSectionX(pwszSection);
    CStrUTF7 strUTF7;               // Assume no UTF7 needed.

    if (strSectionX && pwszKey && pwszValue && !Is7BitClean(pwszValue)) {
        //
        //  The value is not 7-bit clean.  Must create a UTF7 version.
        //
        strUTF7.SetUnicode(pwszValue);
    }

    if (g_bRunningOnNT)
        return WritePrivateProfileStringMultiW(pwszSection, pwszValue,
                                               strSectionX, strUTF7,
                                               pwszKey,     pwszFileName);
    else
        return WritePrivateProfileStringMultiA(pwszSection, pwszValue,
                                               strSectionX, strUTF7,
                                               pwszKey,     pwszFileName);
}

//
//  Keep calling GetPrivateProfileString with bigger and bigger buffers
//  until we get the entire string.  Start with MAX_PATH, since that's
//  usually plenty big enough.
//
//  The returned buffer must be freed with LocalFree, not delete[].
//
LPVOID GetEntirePrivateProfileStringAorW(
    LPCVOID pszSection,
    LPCVOID pszKey,
    LPCVOID pszFileName,
    BOOL    fUnicode)
{
    int    CharSize = fUnicode ? sizeof(WCHAR) : sizeof(CHAR);
    UINT   cchResult = MAX_PATH;
    LPVOID pszResult = LocalAlloc(LMEM_FIXED, cchResult * CharSize);
    LPVOID pszFree = pszResult;

    while (pszResult) {
        UINT cchRc;
        if (fUnicode)
            cchRc = GetPrivateProfileStringW((LPCWSTR)pszSection,
                                             (LPCWSTR)pszKey,
                                             L"",
                                             (LPWSTR)pszResult, cchResult,
                                             (LPCWSTR)pszFileName);
        else
            cchRc = GetPrivateProfileStringA((LPCSTR)pszSection,
                                             (LPCSTR)pszKey,
                                             "",
                                             (LPSTR)pszResult, cchResult,
                                             (LPCSTR)pszFileName);

        if (cchRc < cchResult - 1)
            return pszResult;

        // Buffer too small - iterate
        cchResult *= 2;
        LPVOID pszNew = LocalReAlloc(pszResult, cchResult * CharSize, LMEM_MOVEABLE);
        pszFree = pszResult;
        pszResult = pszNew;
    }

    //
    //  Memory allocation failed; free pszFree while we still can.
    //
    if (pszFree)
        LocalFree(pszFree);
    return NULL;
}

DWORD GetPrivateProfileStringMultiW(LPCWSTR pwszSection, LPCWSTR pwszKey,
                                    LPWSTR pwszSectionA,
                                    LPWSTR pwszReturnedString, DWORD cchSize,
                                    LPCWSTR pwszFileName)
{
    LPWSTR pwszValue  = NULL;
    LPWSTR pwszValueA = NULL;
    LPWSTR pwszUTF7 = NULL;
    DWORD dwRc;

    pwszValue  = (LPWSTR)GetEntirePrivateProfileStringAorW(
                              pwszSection, pwszKey,
                              pwszFileName, TRUE);
    if (pwszValue) {

        //
        //  If the value is an empty string, then don't waste your
        //  time trying to get the UNICODE version - the UNICODE version
        //  of the empty string is the empty string.
        //
        //  Otherwise, get the ANSI shadow hidden in [Section.A]
        //  and see if it matches.  If not, then the file was edited
        //  by a downlevel app and we should just use pwszValue after all.

        if (pwszValue[0] &&
            (pwszValueA = (LPWSTR)GetEntirePrivateProfileStringAorW(
                                      pwszSectionA, pwszKey,
                                      pwszFileName, TRUE)) != NULL &&
            lstrcmpW(pwszValue, pwszValueA) == 0) {

            // our shadow is still good - run with it
            // Change [Section.A] to [Section.W]
            pwszSectionA[lstrlenW(pwszSectionA) - 1] = TEXT('W');

            pwszUTF7 = (LPWSTR)GetEntirePrivateProfileStringAorW(
                                      pwszSectionA, pwszKey,
                                      pwszFileName, TRUE);

            CStrIn strUTF7(pwszUTF7);

            dwRc = 0;                   // Assume something goes wrong

            if (strUTF7) {
                dwRc = SHAnsiToUnicodeCP(CP_UTF7, strUTF7, pwszReturnedString, cchSize);
            }

            if (dwRc == 0) {
                // Problem converting to UTF7 - just use the ANSI version
                dwRc = SHUnicodeToUnicode(pwszValue, pwszReturnedString, cchSize);
            }

        } else {
            // String was empty or couldn't get [Section.A] shadow or
            // shadow doesn't match.  Just use the regular string.
            dwRc = SHUnicodeToUnicode(pwszValue, pwszReturnedString, cchSize);
        }

        // The SHXxxToYyy functions include the terminating zero,
        // which we want to exclude.
        if (dwRc > 0)
            dwRc--;

    } else {
        // Fatal error reading values from file; just use the boring API
        dwRc = GetPrivateProfileStringW(pwszSection,
                                        pwszKey,
                                        L"",
                                        pwszReturnedString, cchSize,
                                        pwszFileName);
    }

    if (pwszValue)
        LocalFree(pwszValue);
    if (pwszValueA)
        LocalFree(pwszValueA);
    if (pwszUTF7)
        LocalFree(pwszUTF7);

    return dwRc;
}

DWORD GetPrivateProfileStringMultiA(LPCWSTR pwszSection, LPCWSTR pwszKey,
                                    LPWSTR pwszSectionA,
                                    LPWSTR pwszReturnedString, DWORD cchSize,
                                    LPCWSTR pwszFileName)
{
    CStrIn strSection(pwszSection);
    CStrIn strSectionA(pwszSectionA);
    CStrIn strKey(pwszKey);
    CPPFIn strFileName(pwszFileName); // PrivateProfile filename needs special class

    LPSTR pszValue  = NULL;
    LPSTR pszValueA = NULL;
    LPSTR pszUTF7   = NULL;
    DWORD dwRc;

    if (strSectionA &&
        (pszValue  = (LPSTR)GetEntirePrivateProfileStringAorW(
                                 strSection, strKey,
                                 strFileName, FALSE)) != NULL) {

        //
        //  If the value is an empty string, then don't waste your
        //  time trying to get the UNICODE version - the UNICODE version
        //  of the empty string is the empty string.
        //
        //  Otherwise, get the ANSI shadow hidden in [Section.A]
        //  and see if it matches.  If not, then the file was edited
        //  by a downlevel app and we should just use pwszValue after all.
        if (pszValue[0] &&
            (pszValueA = (LPSTR)GetEntirePrivateProfileStringAorW(
                                     strSectionA, strKey,
                                     strFileName, FALSE)) != NULL &&
            lstrcmpA(pszValue, pszValueA) == 0) {

            // our shadow is still good - run with it
            // Change [Section.A] to [Section.W]

            strSectionA[lstrlenA(strSectionA) - 1] = 'W';

            pszUTF7 = (LPSTR)GetEntirePrivateProfileStringAorW(
                                       strSectionA, strKey,
                                       strFileName, FALSE);
            dwRc = 0;                   // Assume something goes wrong

            if (pszUTF7) {
                dwRc = SHAnsiToUnicodeCP(CP_UTF7, pszUTF7, pwszReturnedString, cchSize);
            }

            if (dwRc == 0) {
                // Problem converting to UTF7 - just use the ANSI version
                dwRc = SHAnsiToUnicode(pszValue, pwszReturnedString, cchSize);
            }

        } else {
            // String was empty or couldn't get [Section.A] shadow or
            // shadow doesn't match.  Just use the regular string.
            dwRc = SHAnsiToUnicode(pszValue, pwszReturnedString, cchSize);
        }

        // The SHXxxToYyy functions include the terminating zero,
        // which we want to exclude.
        if (dwRc > 0)
            dwRc--;

    } else {
        // Fatal error reading values from file; just use the boring API
        CStrOut strOut(pwszReturnedString, cchSize);
        dwRc = GetPrivateProfileStringA(strSection,
                                        strKey,
                                        "",
                                        strOut, cchSize,
                                        strFileName);
        strOut.ConvertIncludingNul();
    }

    if (pszValue)
        LocalFree(pszValue);
    if (pszValueA)
        LocalFree(pszValueA);
    if (pszUTF7)
        LocalFree(pszUTF7);


    return dwRc;
}

DWORD WINAPI SHGetIniStringW(LPCWSTR pwszSection, LPCWSTR pwszKey, LPWSTR pwszReturnedString, DWORD cchSize, LPCWSTR pwszFileName)
{
    VALIDATE_OUTBUF(pwszReturnedString, cchSize);

    // We have no way of encoding these two, so they had better by 7-bit clean
    // We also do not support "get all section names" or "get entire section".
    AssertMsg(pwszSection != NULL,
              TEXT("SHGetIniStringW: Section name cannot be NULL; bug in caller"));
    AssertMsg(Is7BitClean(pwszSection),
              TEXT("SHGetIniStringW: Section name not 7-bit clean; bug in caller"));
    AssertMsg(pwszKey != NULL,
              TEXT("SHGetIniStringW: Key name cannot be NULL; bug in caller"));
    AssertMsg(Is7BitClean(pwszKey),
              TEXT("SHGetIniStringW: Key name not 7-bit clean; bug in caller"));

    CStrSectionX strSectionX(pwszSection);

    if (g_bRunningOnNT)
        return GetPrivateProfileStringMultiW(pwszSection, pwszKey,
                                             strSectionX,
                                             pwszReturnedString, cchSize,
                                             pwszFileName);
    else
        return GetPrivateProfileStringMultiA(pwszSection, pwszKey,
                                             strSectionX,
                                             pwszReturnedString, cchSize,
                                             pwszFileName);
}

//+---------------------------------------------------------------------------
//
//  CreateURLFileContents
//
//  shdocvw.dll and url.dll need to create in-memory images
//  of URL files, so this helper function does all the grunky work so they
//  can remain insulated from the way we encode Unicode strings into INI files.
//  The resulting memory should be freed via GlobalFree().

//
//  Writes a string into the URL file.  If fWrite is FALSE, then
//  then just do the math and don't actually write anything.  This lets us
//  use one function to handle both the measurement pass and the rendering
//  pass.
//
LPSTR AddToURLFileContents(LPSTR pszFile, LPCSTR psz, BOOL fWrite)
{
    int cch = lstrlenA(psz);
    if (fWrite) {
        memcpy(pszFile, psz, cch);
    }
    pszFile += cch;
    return pszFile;
}

LPSTR AddURLFileSection(LPSTR pszFile, LPCSTR pszSuffix, LPCSTR pszUrl, BOOL fWrite)
{
    pszFile = AddToURLFileContents(pszFile, "[InternetShortcut", fWrite);
    pszFile = AddToURLFileContents(pszFile, pszSuffix, fWrite);
    pszFile = AddToURLFileContents(pszFile, "]\r\nURL=", fWrite);
    pszFile = AddToURLFileContents(pszFile, pszUrl, fWrite);
    pszFile = AddToURLFileContents(pszFile, "\r\n", fWrite);
    return pszFile;
}

//
//  The file consists of an [InternetShortcut] section, followed if
//  necessary by [InternetShortcut.A] and [InternetShortcut.W].
//
LPSTR AddURLFileContents(LPSTR pszFile, LPCSTR pszUrl, LPCSTR pszUTF7, BOOL fWrite)
{
    pszFile = AddURLFileSection(pszFile, "", pszUrl, fWrite);
    if (pszUTF7) {
        pszFile = AddURLFileSection(pszFile, ".A", pszUrl, fWrite);
        pszFile = AddURLFileSection(pszFile, ".W", pszUTF7, fWrite);
    }
    return pszFile;
}

//
//  Returns number of bytes in file contents (not including trailing NULL),
//  or an OLE error code.  If ppszOut is NULL, then no contents are returned.
//
HRESULT GenerateURLFileContents(LPCWSTR pwszUrl, LPCSTR pszUrl, LPSTR *ppszOut)
{
    HRESULT hr = 0;

    if (ppszOut)
        *ppszOut = NULL;

    if (pwszUrl && pszUrl) {
        CStrUTF7 strUTF7;               // Assume no UTF7 needed.
        if (!Is7BitClean(pwszUrl)) {
            //
            //  The value is not 7-bit clean.  Must create a UTF7 version.
            //
            strUTF7.SetUnicode(pwszUrl);
        }

        hr = PtrToUlong(AddURLFileContents(NULL, pszUrl, strUTF7, FALSE));
        ASSERT(SUCCEEDED(hr));

        if (ppszOut) {
            *ppszOut = (LPSTR)GlobalAlloc(GMEM_FIXED, hr + 1);
            if (*ppszOut) {
                LPSTR pszEnd = AddURLFileContents(*ppszOut, pszUrl, strUTF7, TRUE);
                *pszEnd = '\0';
            } else {
                hr = E_OUTOFMEMORY;
            }
        }
    }

    //
    //  Double-check the value we return.
    //
    if (SUCCEEDED(hr) && ppszOut) {
        ASSERT(hr == lstrlenA(*ppszOut));
    }

    return hr;
}


HRESULT CreateURLFileContentsW(LPCWSTR pwszUrl, LPSTR *ppszOut)
{
    CStrIn strUrl(pwszUrl);
    return GenerateURLFileContents(pwszUrl, strUrl, ppszOut);
}

HRESULT CreateURLFileContentsA(LPCSTR pszUrl, LPSTR *ppszOut)
{
    CStrInW strUrl(pszUrl);
    return GenerateURLFileContents(strUrl, pszUrl, ppszOut);
}

DWORD SHGetIniStringUTF7W(LPCWSTR lpSection, LPCWSTR lpKey, LPWSTR lpBuf, DWORD nSize, LPCWSTR lpFile)
{
    if (*lpKey == CH_CANBEUNICODEW)
        return SHGetIniStringW(lpSection, lpKey+1, lpBuf, nSize, lpFile);
    else
        return GetPrivateProfileStringWrapW(lpSection, lpKey, L"", lpBuf, nSize, lpFile);
}

BOOL SHSetIniStringUTF7W(LPCWSTR lpSection, LPCWSTR lpKey, LPCWSTR lpString, LPCWSTR lpFile)
{
    if (*lpKey == CH_CANBEUNICODEW)
        return SHSetIniStringW(lpSection, lpKey+1, lpString, lpFile);
    else
        return WritePrivateProfileStringWrapW(lpSection, lpKey, lpString, lpFile);
}

