/* Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#ifdef WIN32

#include "apr.h"
#include "arch/win32/apr_arch_file_io.h"
#include "arch/win32/apr_arch_misc.h"
#include "ap_regkey.h"

struct ap_regkey_t {
    apr_pool_t *pool;
    HKEY        hkey;
};


AP_DECLARE(const ap_regkey_t *) ap_regkey_const(int i)
{
    static struct ap_regkey_t ap_regkey_consts[7] =
    {
        {NULL, HKEY_CLASSES_ROOT},
        {NULL, HKEY_CURRENT_CONFIG},
        {NULL, HKEY_CURRENT_USER},
        {NULL, HKEY_LOCAL_MACHINE},
        {NULL, HKEY_USERS},
        {NULL, HKEY_PERFORMANCE_DATA},
        {NULL, HKEY_DYN_DATA}
    };
    return ap_regkey_consts + i;
}


static apr_status_t regkey_cleanup(void *key)
{
    ap_regkey_t *regkey = key;

    if (regkey->hkey && regkey->hkey != INVALID_HANDLE_VALUE) {
        RegCloseKey(regkey->hkey);
        regkey->hkey = INVALID_HANDLE_VALUE;
    }
    return APR_SUCCESS;
}


AP_DECLARE(apr_status_t) ap_regkey_open(ap_regkey_t **newkey,
                                        const ap_regkey_t *parentkey,
                                        const char *keyname,
                                        apr_int32_t flags,
                                        apr_pool_t *pool)
{
    DWORD access = KEY_QUERY_VALUE;
    DWORD exists;
    HKEY hkey;
    LONG rc;

    if (flags & APR_READ)
        access |= KEY_READ;
    if (flags & APR_WRITE)
        access |= KEY_WRITE;

#if APR_HAS_UNICODE_FS
    IF_WIN_OS_IS_UNICODE
    {
        apr_size_t keylen = strlen(keyname) + 1;
        apr_size_t wkeylen = 256;
        apr_wchar_t wkeyname[256];
        apr_status_t rv = apr_conv_utf8_to_ucs2(keyname, &keylen, wkeyname, &wkeylen);
        if (rv != APR_SUCCESS)
            return rv;
        else if (keylen)
            return APR_ENAMETOOLONG;

        if (flags & APR_CREATE)
            rc = RegCreateKeyExW(parentkey->hkey, wkeyname, 0, NULL, 0,
                                 access, NULL, &hkey, &exists);
        else
            rc = RegOpenKeyExW(parentkey->hkey, wkeyname, 0, access, &hkey);
    }
#endif /* APR_HAS_UNICODE_FS */
#if APR_HAS_ANSI_FS
    ELSE_WIN_OS_IS_ANSI
    {
        if (flags & APR_CREATE)
            rc = RegCreateKeyEx(parentkey->hkey, keyname, 0, NULL, 0,
                                access, NULL, &hkey, &exists);
        else
            rc = RegOpenKeyEx(parentkey->hkey, keyname, 0, access, &hkey);
    }
#endif
    if (rc != ERROR_SUCCESS) {
        return APR_FROM_OS_ERROR(rc);
    }
    if ((flags & APR_EXCL) && (exists == REG_OPENED_EXISTING_KEY)) {
        RegCloseKey(hkey);
        return APR_EEXIST;
    }

    *newkey = apr_palloc(pool, sizeof(**newkey));
    (*newkey)->pool = pool;
    (*newkey)->hkey = hkey;
    apr_pool_cleanup_register((*newkey)->pool, (void *)(*newkey),
                              regkey_cleanup, apr_pool_cleanup_null);
    return APR_SUCCESS;
}


AP_DECLARE(apr_status_t) ap_regkey_close(ap_regkey_t *regkey)
{
    apr_status_t stat;
    if ((stat = regkey_cleanup(regkey)) == APR_SUCCESS) {
        apr_pool_cleanup_kill(regkey->pool, regkey, regkey_cleanup);
    }
    return stat;
}


AP_DECLARE(apr_status_t) ap_regkey_remove(const ap_regkey_t *parent,
                                          const char *keyname,
                                          apr_pool_t *pool)
{
    LONG rc;

#if APR_HAS_UNICODE_FS
    IF_WIN_OS_IS_UNICODE
    {
        apr_size_t keylen = strlen(keyname) + 1;
        apr_size_t wkeylen = 256;
        apr_wchar_t wkeyname[256];
        apr_status_t rv = apr_conv_utf8_to_ucs2(keyname, &keylen, wkeyname, &wkeylen);
        if (rv != APR_SUCCESS)
            return rv;
        else if (keylen)
            return APR_ENAMETOOLONG;
        rc = RegDeleteKeyW(parent->hkey, wkeyname);
    }
#endif /* APR_HAS_UNICODE_FS */
#if APR_HAS_ANSI_FS
    ELSE_WIN_OS_IS_ANSI
    {
        /* We need to determine if subkeys exist on Win9x, to provide
         * consistent behavior with NT, which returns access denied
         * if subkeys exist when attempting to delete a key.
         */
        DWORD subkeys;
        HKEY hkey;
        rc = RegOpenKeyEx(parent->hkey, keyname, 0, KEY_READ, &hkey);
        if (rc != ERROR_SUCCESS)
            return APR_FROM_OS_ERROR(rc);
        rc = RegQueryInfoKey(hkey, NULL, NULL, NULL, &subkeys, NULL, NULL,
                             NULL, NULL, NULL, NULL, NULL);
        RegCloseKey(hkey);
        if (rc != ERROR_SUCCESS)
            return APR_FROM_OS_ERROR(rc);
        else if (subkeys)
            return APR_FROM_OS_ERROR(ERROR_ACCESS_DENIED);
        rc = RegDeleteKey(parent->hkey, keyname);
    }
#endif
    if (rc != ERROR_SUCCESS) {
        return APR_FROM_OS_ERROR(rc);
    }
    return APR_SUCCESS;
}


AP_DECLARE(apr_status_t) ap_regkey_value_get(char **result,
                                             ap_regkey_t *key,
                                             const char *valuename,
                                             apr_pool_t *pool)
{
    /* Retrieve a registry string value, and explode any envvars
     * that the system has configured (e.g. %SystemRoot%/someapp.exe)
     */
    LONG rc;
    DWORD type;
    apr_size_t size = 0;

#if APR_HAS_UNICODE_FS
    IF_WIN_OS_IS_UNICODE
    {
        apr_size_t valuelen = strlen(valuename) + 1;
        apr_size_t wvallen = 256;
        apr_wchar_t wvalname[256];
        apr_wchar_t *wvalue;
        apr_status_t rv;
        rv = apr_conv_utf8_to_ucs2(valuename, &valuelen, wvalname, &wvallen);
        if (rv != APR_SUCCESS)
            return rv;
        else if (valuelen)
            return APR_ENAMETOOLONG;
        /* Read to NULL buffer to determine value size */
        rc = RegQueryValueExW(key->hkey, wvalname, 0, &type, NULL, (DWORD *)&size);
        if (rc != ERROR_SUCCESS) {
            return APR_FROM_OS_ERROR(rc);
        }
        if ((size < 2) || (type != REG_SZ && type != REG_EXPAND_SZ)) {
            return APR_FROM_OS_ERROR(ERROR_INVALID_PARAMETER);
        }

        wvalue = apr_palloc(pool, size);
        /* Read value based on size query above */
        rc = RegQueryValueExW(key->hkey, wvalname, 0, &type,
                              (LPBYTE)wvalue, (DWORD *)&size);
        if (rc != ERROR_SUCCESS) {
            return APR_FROM_OS_ERROR(rc);
        }
        if (type == REG_EXPAND_SZ) {
            apr_wchar_t zbuf[1];
            size = ExpandEnvironmentStringsW(wvalue, zbuf, 0);
            if (size) {
                apr_wchar_t *tmp = wvalue;
                /* The size returned by ExpandEnvironmentStringsW is wchars */
                wvalue = apr_palloc(pool, size * 2);
                size = ExpandEnvironmentStringsW(tmp, wvalue, (DWORD)size);
            }
        }
        else {
            /* count wchars from RegQueryValueExW, rather than bytes */
            size /= 2;
        }
        /* ###: deliberately overallocate all but the trailing null.
         * We could precalculate the exact buffer here instead, the question
         * is a matter of storage v.s. cpu cycles.
         */
        valuelen = (size - 1) * 3 + 1;
        *result = apr_palloc(pool, valuelen);
        rv = apr_conv_ucs2_to_utf8(wvalue, &size, *result, &valuelen);
        if (rv != APR_SUCCESS)
            return rv;
        else if (size)
            return APR_ENAMETOOLONG;
    }
#endif /* APR_HAS_UNICODE_FS */
#if APR_HAS_ANSI_FS
    ELSE_WIN_OS_IS_ANSI
    {
        /* Read to NULL buffer to determine value size */
        rc = RegQueryValueEx(key->hkey, valuename, 0, &type, NULL, (DWORD *)&size);
        if (rc != ERROR_SUCCESS)
            return APR_FROM_OS_ERROR(rc);

        if ((size < 1) || (type != REG_SZ && type != REG_EXPAND_SZ)) {
            return APR_FROM_OS_ERROR(ERROR_INVALID_PARAMETER);
        }

        *result = apr_palloc(pool, size);
        /* Read value based on size query above */
        rc = RegQueryValueEx(key->hkey, valuename, 0, &type, *result, (DWORD *)&size);
        if (rc != ERROR_SUCCESS)
            return APR_FROM_OS_ERROR(rc);

        if (type == REG_EXPAND_SZ) {
            /* Advise ExpandEnvironmentStrings that we have a zero char
             * buffer to force computation of the required length.
             */
            char zbuf[1];
            size = ExpandEnvironmentStrings(*result, zbuf, 0);
            if (size) {
                char *tmp = *result;
                *result = apr_palloc(pool, size);
                size = ExpandEnvironmentStrings(tmp, *result, (DWORD)size);
            }
        }
    }
#endif
    return APR_SUCCESS;
}


AP_DECLARE(apr_status_t) ap_regkey_value_set(ap_regkey_t *key,
                                             const char *valuename,
                                             const char *value,
                                             apr_int32_t flags,
                                             apr_pool_t *pool)
{
    /* Retrieve a registry string value, and explode any envvars
     * that the system has configured (e.g. %SystemRoot%/someapp.exe)
     */
    LONG rc;
    apr_size_t size = strlen(value) + 1;
    DWORD type = (flags & AP_REGKEY_EXPAND) ? REG_EXPAND_SZ : REG_SZ;

#if APR_HAS_UNICODE_FS
    IF_WIN_OS_IS_UNICODE
    {
        apr_size_t alloclen;
        apr_size_t valuelen = strlen(valuename) + 1;
        apr_size_t wvallen = 256;
        apr_wchar_t wvalname[256];
        apr_wchar_t *wvalue;
        apr_status_t rv;
        rv = apr_conv_utf8_to_ucs2(valuename, &valuelen, wvalname, &wvallen);
        if (rv != APR_SUCCESS)
            return rv;
        else if (valuelen)
            return APR_ENAMETOOLONG;

        wvallen = alloclen = size;
        wvalue = apr_palloc(pool, alloclen * 2);
        rv = apr_conv_utf8_to_ucs2(value, &size, wvalue, &wvallen);
        if (rv != APR_SUCCESS)
            return rv;
        else if (size)
            return APR_ENAMETOOLONG;

        /* The size is the number of wchars consumed by apr_conv_utf8_to_ucs2
         * converted to bytes; the trailing L'\0' continues to be counted.
         */
        size = (alloclen - wvallen) * 2;
        rc = RegSetValueExW(key->hkey, wvalname, 0, type,
                            (LPBYTE)wvalue, (DWORD)size);
        if (rc != ERROR_SUCCESS)
            return APR_FROM_OS_ERROR(rc);
    }
#endif /* APR_HAS_UNICODE_FS */
#if APR_HAS_ANSI_FS
    ELSE_WIN_OS_IS_ANSI
    {
        rc = RegSetValueEx(key->hkey, valuename, 0, type, value, (DWORD)size);
        if (rc != ERROR_SUCCESS)
            return APR_FROM_OS_ERROR(rc);
    }
#endif
    return APR_SUCCESS;
}


AP_DECLARE(apr_status_t) ap_regkey_value_raw_get(void **result,
                                                 apr_size_t *resultsize,
                                                 apr_int32_t *resulttype,
                                                 ap_regkey_t *key,
                                                 const char *valuename,
                                                 apr_pool_t *pool)
{
    /* Retrieve a registry string value, and explode any envvars
     * that the system has configured (e.g. %SystemRoot%/someapp.exe)
     */
    LONG rc;

#if APR_HAS_UNICODE_FS
    IF_WIN_OS_IS_UNICODE
    {
        apr_size_t valuelen = strlen(valuename) + 1;
        apr_size_t wvallen = 256;
        apr_wchar_t wvalname[256];
        apr_status_t rv;
        rv = apr_conv_utf8_to_ucs2(valuename, &valuelen, wvalname, &wvallen);
        if (rv != APR_SUCCESS)
            return rv;
        else if (valuelen)
            return APR_ENAMETOOLONG;
        /* Read to NULL buffer to determine value size */
        rc = RegQueryValueExW(key->hkey, wvalname, 0, (LPDWORD)resulttype,
                              NULL, (LPDWORD)resultsize);
        if (rc != ERROR_SUCCESS) {
            return APR_FROM_OS_ERROR(rc);
        }

        /* Read value based on size query above */
        *result = apr_palloc(pool, *resultsize);
        rc = RegQueryValueExW(key->hkey, wvalname, 0, (LPDWORD)resulttype,
                             (LPBYTE)*result, (LPDWORD)resultsize);
    }
#endif /* APR_HAS_UNICODE_FS */
#if APR_HAS_ANSI_FS
    ELSE_WIN_OS_IS_ANSI
    {
        /* Read to NULL buffer to determine value size */
        rc = RegQueryValueEx(key->hkey, valuename, 0, (LPDWORD)resulttype,
                             NULL, (LPDWORD)resultsize);
        if (rc != ERROR_SUCCESS)
            return APR_FROM_OS_ERROR(rc);

        /* Read value based on size query above */
        *result = apr_palloc(pool, *resultsize);
        rc = RegQueryValueEx(key->hkey, valuename, 0, (LPDWORD)resulttype,
                             (LPBYTE)*result, (LPDWORD)resultsize);
        if (rc != ERROR_SUCCESS)
            return APR_FROM_OS_ERROR(rc);
    }
#endif
    if (rc != ERROR_SUCCESS) {
        return APR_FROM_OS_ERROR(rc);
    }

    return APR_SUCCESS;
}


AP_DECLARE(apr_status_t) ap_regkey_value_raw_set(ap_regkey_t *key,
                                                 const char *valuename,
                                                 const void *value,
                                                 apr_size_t valuesize,
                                                 apr_int32_t valuetype,
                                                 apr_pool_t *pool)
{
    LONG rc;

#if APR_HAS_UNICODE_FS
    IF_WIN_OS_IS_UNICODE
    {
        apr_size_t valuelen = strlen(valuename) + 1;
        apr_size_t wvallen = 256;
        apr_wchar_t wvalname[256];
        apr_status_t rv;
        rv = apr_conv_utf8_to_ucs2(valuename, &valuelen, wvalname, &wvallen);
        if (rv != APR_SUCCESS)
            return rv;
        else if (valuelen)
            return APR_ENAMETOOLONG;

        rc = RegSetValueExW(key->hkey, wvalname, 0, valuetype,
                            (LPBYTE)value, (DWORD)valuesize);
    }
#endif /* APR_HAS_UNICODE_FS */
#if APR_HAS_ANSI_FS
    ELSE_WIN_OS_IS_ANSI
    {
        rc = RegSetValueEx(key->hkey, valuename, 0, valuetype,
                            (LPBYTE)value, (DWORD)valuesize);
    }
#endif
    if (rc != ERROR_SUCCESS) {
        return APR_FROM_OS_ERROR(rc);
    }
    return APR_SUCCESS;
}


AP_DECLARE(apr_status_t) ap_regkey_value_array_get(apr_array_header_t **result,
                                                   ap_regkey_t *key,
                                                   const char *valuename,
                                                   apr_pool_t *pool)
{
    /* Retrieve a registry string value, and explode any envvars
     * that the system has configured (e.g. %SystemRoot%/someapp.exe)
     */
    apr_status_t rv;
    void *value;
    char *buf;
    char *tmp;
    apr_int32_t type;
    apr_size_t size = 0;

    rv = ap_regkey_value_raw_get(&value, &size, &type, key, valuename, pool);
    if (rv != APR_SUCCESS) {
        return rv;
    }
    else if (type != REG_MULTI_SZ) {
        return APR_EINVAL;
    }

#if APR_HAS_UNICODE_FS
    IF_WIN_OS_IS_UNICODE
    {
        apr_size_t alloclen;
        apr_size_t valuelen = strlen(valuename) + 1;

        /* ###: deliberately overallocate plus two extra nulls.
         * We could precalculate the exact buffer here instead, the question
         * is a matter of storage v.s. cpu cycles.
         */
        size /= 2;
        alloclen = valuelen = size * 3 + 2;
        buf = apr_palloc(pool, valuelen);
        rv = apr_conv_ucs2_to_utf8(value, &size, buf, &valuelen);
        if (rv != APR_SUCCESS)
            return rv;
        else if (size)
            return APR_ENAMETOOLONG;
        buf[(alloclen - valuelen)] = '\0';
        buf[(alloclen - valuelen) + 1] = '\0';
    }
#endif /* APR_HAS_UNICODE_FS */
#if APR_HAS_ANSI_FS
    ELSE_WIN_OS_IS_ANSI
    {
        /* Small possibility the array is either unterminated
         * or single NULL terminated.  Avert.
         */
        buf = (char *)value;
        if (size < 2 || buf[size - 1] != '\0' || buf[size - 2] != '\0') {
            buf = apr_palloc(pool, size + 2);
            memcpy(buf, value, size);
            buf[size + 1] = '\0';
            buf[size] = '\0';
        }
    }
#endif

    size = 0;    /* Element Count */
    for (tmp = buf; *tmp; ++tmp) {
        ++size;
        while (*tmp) {
            ++tmp;
        }
    }

    *result = apr_array_make(pool, (int)size, sizeof(char *));
    for (tmp = buf; *tmp; ++tmp) {
        char **newelem = (char **) apr_array_push(*result);
        *newelem = tmp;
        while (*tmp) {
            ++tmp;
        }
    }

   return APR_SUCCESS;
}


AP_DECLARE(apr_status_t) ap_regkey_value_array_set(ap_regkey_t *key,
                                                   const char *valuename,
                                                   int nelts,
                                                   const char * const * elts,
                                                   apr_pool_t *pool)
{
    /* Retrieve a registry string value, and explode any envvars
     * that the system has configured (e.g. %SystemRoot%/someapp.exe)
     */
    int i;
    const void *value;
    apr_size_t bufsize;

#if APR_HAS_UNICODE_FS
    IF_WIN_OS_IS_UNICODE
    {
        apr_status_t rv;
        apr_wchar_t *buf;
        apr_wchar_t *tmp;
        apr_size_t bufrem;

        bufsize = 1; /* For trailing second null */
        for (i = 0; i < nelts; ++i) {
            bufsize += strlen(elts[i]) + 1;
        }
        if (!nelts) {
            ++bufsize;
        }

        bufrem = bufsize;
        buf = apr_palloc(pool, bufsize * 2);
        tmp = buf;
        for (i = 0; i < nelts; ++i) {
            apr_size_t eltsize = strlen(elts[i]) + 1;
            apr_size_t size = eltsize;
            rv = apr_conv_utf8_to_ucs2(elts[i], &size, tmp, &bufrem);
            if (rv != APR_SUCCESS)
                return rv;
            else if (size)
                return APR_ENAMETOOLONG;
            tmp += eltsize;
        }
        if (!nelts) {
            --bufrem;
            (*tmp++) = L'\0';
        }
        --bufrem;
        *tmp = L'\0'; /* Trailing second null */

        bufsize = (bufsize - bufrem) * 2;
        value = (void*)buf;
    }
#endif /* APR_HAS_UNICODE_FS */
#if APR_HAS_ANSI_FS
    ELSE_WIN_OS_IS_ANSI
    {
        char *buf;
        char *tmp;

        bufsize = 1; /* For trailing second null */
        for (i = 0; i < nelts; ++i) {
            bufsize += strlen(elts[i]) + 1;
        }
        if (!nelts) {
            ++bufsize;
        }
        buf = apr_palloc(pool, bufsize);
        tmp = buf;
        for (i = 0; i < nelts; ++i) {
            apr_size_t len = strlen(elts[i]) + 1;
            memcpy(tmp, elts[i], len);
            tmp += len;
        }
        if (!nelts) {
            (*tmp++) = '\0';
        }
        *tmp = '\0'; /* Trailing second null */
        value = buf;
    }
#endif
    return ap_regkey_value_raw_set(key, valuename, value,
                                   bufsize, REG_MULTI_SZ, pool);
}


AP_DECLARE(apr_status_t) ap_regkey_value_remove(const ap_regkey_t *key,
                                                const char *valuename,
                                                apr_pool_t *pool)
{
    LONG rc;

#if APR_HAS_UNICODE_FS
    IF_WIN_OS_IS_UNICODE
    {
        apr_size_t valuelen = strlen(valuename) + 1;
        apr_size_t wvallen = 256;
        apr_wchar_t wvalname[256];
        apr_status_t rv = apr_conv_utf8_to_ucs2(valuename, &valuelen, wvalname, &wvallen);
        if (rv != APR_SUCCESS)
            return rv;
        else if (valuelen)
            return APR_ENAMETOOLONG;
        rc = RegDeleteValueW(key->hkey, wvalname);
    }
#endif /* APR_HAS_UNICODE_FS */
#if APR_HAS_ANSI_FS
    ELSE_WIN_OS_IS_ANSI
    {
        rc = RegDeleteValue(key->hkey, valuename);
    }
#endif
    if (rc != ERROR_SUCCESS) {
        return APR_FROM_OS_ERROR(rc);
    }
    return APR_SUCCESS;
}

#endif /* defined WIN32 */
