/**
 * FreeRDP: A Remote Desktop Protocol Implementation
 * Clipboard Virtual Channel Extension
 *
 * Copyright 2013 Marc-Andre Moreau <marcandre.moreau@gmail.com>
 * Copyright 2022 Armin Novak <anovak@thincast.com
 * Copyright 2022 Thincast Technologies GmbH
 *
 * Licensed 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.
 */

#include <winpr/stream.h>
#include <freerdp/utils/cliprdr_utils.h>
#include <freerdp/channels/cliprdr.h>

#include <freerdp/log.h>
#define TAG FREERDP_TAG("utils." CLIPRDR_SVC_CHANNEL_NAME)

#define CLIPRDR_FILEDESCRIPTOR_SIZE (4 + 32 + 4 + 16 + 8 + 8 + 520)
#define CLIPRDR_MAX_FILE_SIZE (2U * 1024 * 1024 * 1024)

static UINT64 filetime_to_uint64(FILETIME value)
{
	UINT64 converted = 0;
	converted |= (UINT32)value.dwHighDateTime;
	converted <<= 32;
	converted |= (UINT32)value.dwLowDateTime;
	return converted;
}

static FILETIME uint64_to_filetime(UINT64 value)
{
	FILETIME converted;
	converted.dwLowDateTime = (UINT32)(value >> 0);
	converted.dwHighDateTime = (UINT32)(value >> 32);
	return converted;
}

/**
 * Parse a packed file list.
 *
 * The resulting array must be freed with the `free()` function.
 *
 * @param [in]  format_data            packed `CLIPRDR_FILELIST` to parse.
 * @param [in]  format_data_length     length of `format_data` in bytes.
 * @param [out] file_descriptor_array  parsed array of `FILEDESCRIPTOR` structs.
 * @param [out] file_descriptor_count  number of elements in `file_descriptor_array`.
 *
 * @returns 0 on success, otherwise a Win32 error code.
 */
UINT cliprdr_parse_file_list(const BYTE* format_data, UINT32 format_data_length,
                             FILEDESCRIPTORW** file_descriptor_array, UINT32* file_descriptor_count)
{
	UINT result = NO_ERROR;
	UINT32 count = 0;
	wStream sbuffer;
	wStream* s = nullptr;

	if (!format_data || !file_descriptor_array || !file_descriptor_count)
		return ERROR_BAD_ARGUMENTS;

	s = Stream_StaticConstInit(&sbuffer, format_data, format_data_length);
	if (!s)
		return ERROR_NOT_ENOUGH_MEMORY;

	if (!Stream_CheckAndLogRequiredLength(TAG, s, 4))
	{
		result = ERROR_INCORRECT_SIZE;
		goto out;
	}

	Stream_Read_UINT32(s, count); /* cItems (4 bytes) */

	if (!Stream_CheckAndLogRequiredLengthOfSize(TAG, s, count, CLIPRDR_FILEDESCRIPTOR_SIZE))
	{
		result = ERROR_INCORRECT_SIZE;
		goto out;
	}

	*file_descriptor_count = count;
	*file_descriptor_array = calloc(count, sizeof(FILEDESCRIPTORW));
	if (!*file_descriptor_array)
	{
		result = ERROR_NOT_ENOUGH_MEMORY;
		goto out;
	}

	for (UINT32 i = 0; i < count; i++)
	{
		FILEDESCRIPTORW* file = &((*file_descriptor_array)[i]);

		if (!cliprdr_read_filedescriptor(s, file))
			goto out;
	}

	if (Stream_GetRemainingLength(s) > 0)
		WLog_WARN(TAG, "packed file list has %" PRIuz " excess bytes",
		          Stream_GetRemainingLength(s));
out:

	return result;
}

BOOL cliprdr_read_filedescriptor(wStream* s, FILEDESCRIPTORW* descriptor)
{
	UINT64 tmp = 0;
	WINPR_ASSERT(descriptor);

	if (!Stream_CheckAndLogRequiredLength(TAG, s, sizeof(FILEDESCRIPTORW)))
		return FALSE;

	Stream_Read_UINT32(s, descriptor->dwFlags); /* flags (4 bytes) */
	Stream_Read_UINT32(s, descriptor->clsid.Data1);
	Stream_Read_UINT16(s, descriptor->clsid.Data2);
	Stream_Read_UINT16(s, descriptor->clsid.Data3);
	Stream_Read(s, &descriptor->clsid.Data4, sizeof(descriptor->clsid.Data4));
	Stream_Read_INT32(s, descriptor->sizel.cx);
	Stream_Read_INT32(s, descriptor->sizel.cy);
	Stream_Read_INT32(s, descriptor->pointl.x);
	Stream_Read_INT32(s, descriptor->pointl.y);
	Stream_Read_UINT32(s, descriptor->dwFileAttributes); /* fileAttributes (4 bytes) */
	Stream_Read_UINT64(s, tmp);                    /* ftCreationTime (8 bytes) */
	descriptor->ftCreationTime = uint64_to_filetime(tmp);
	Stream_Read_UINT64(s, tmp); /* ftLastAccessTime (8 bytes) */
	descriptor->ftLastAccessTime = uint64_to_filetime(tmp);
	Stream_Read_UINT64(s, tmp); /* lastWriteTime (8 bytes) */
	descriptor->ftLastWriteTime = uint64_to_filetime(tmp);
	Stream_Read_UINT32(s, descriptor->nFileSizeHigh); /* fileSizeHigh (4 bytes) */
	Stream_Read_UINT32(s, descriptor->nFileSizeLow);  /* fileSizeLow (4 bytes) */
	return Stream_Read_UTF16_String(s, descriptor->cFileName,
	                                ARRAYSIZE(descriptor->cFileName)); /* cFileName (520 bytes) */
}

BOOL cliprdr_write_filedescriptor(wStream* s, const FILEDESCRIPTORW* descriptor)
{
	WINPR_ASSERT(descriptor);

	if (!Stream_EnsureRemainingCapacity(s, sizeof(FILEDESCRIPTORW)))
		return FALSE;

	Stream_Write_UINT32(s, descriptor->dwFlags); /* flags (4 bytes) */

	Stream_Write_UINT32(s, descriptor->clsid.Data1);
	Stream_Write_UINT16(s, descriptor->clsid.Data2);
	Stream_Write_UINT16(s, descriptor->clsid.Data3);
	Stream_Write(s, &descriptor->clsid.Data4, sizeof(descriptor->clsid.Data4));
	Stream_Write_INT32(s, descriptor->sizel.cx);
	Stream_Write_INT32(s, descriptor->sizel.cy);
	Stream_Write_INT32(s, descriptor->pointl.x);
	Stream_Write_INT32(s, descriptor->pointl.y);
	Stream_Write_UINT32(s, descriptor->dwFileAttributes); /* fileAttributes (4 bytes) */
	Stream_Write_UINT64(s, filetime_to_uint64(descriptor->ftCreationTime));
	Stream_Write_UINT64(s, filetime_to_uint64(descriptor->ftLastAccessTime));
	Stream_Write_UINT64(
	    s, filetime_to_uint64(descriptor->ftLastWriteTime)); /* lastWriteTime (8 bytes) */
	Stream_Write_UINT32(s, descriptor->nFileSizeHigh);       /* fileSizeHigh (4 bytes) */
	Stream_Write_UINT32(s, descriptor->nFileSizeLow);        /* fileSizeLow (4 bytes) */
	return Stream_Write_UTF16_String(s, descriptor->cFileName,
	                                 ARRAYSIZE(descriptor->cFileName)); /* cFileName (520 bytes) */
}

/**
 * Serialize a packed file list.
 *
 * The resulting format data must be freed with the `free()` function.
 *
 * @param [in]  file_descriptor_array  array of `FILEDESCRIPTOR` structs to serialize.
 * @param [in]  file_descriptor_count  number of elements in `file_descriptor_array`.
 * @param [out] format_data            serialized CLIPRDR_FILELIST.
 * @param [out] format_data_length     length of `format_data` in bytes.
 *
 * @returns 0 on success, otherwise a Win32 error code.
 */
UINT cliprdr_serialize_file_list(const FILEDESCRIPTORW* file_descriptor_array,
                                 UINT32 file_descriptor_count, BYTE** format_data,
                                 UINT32* format_data_length)
{
	return cliprdr_serialize_file_list_ex(CB_STREAM_FILECLIP_ENABLED, file_descriptor_array,
	                                      file_descriptor_count, format_data, format_data_length);
}

UINT cliprdr_serialize_file_list_ex(UINT32 flags, const FILEDESCRIPTORW* file_descriptor_array,
                                    UINT32 file_descriptor_count, BYTE** format_data,
                                    UINT32* format_data_length)
{
	UINT result = NO_ERROR;
	size_t len = 0;
	wStream* s = nullptr;

	if (!file_descriptor_array || !format_data || !format_data_length)
		return ERROR_BAD_ARGUMENTS;

	if ((flags & CB_STREAM_FILECLIP_ENABLED) == 0)
	{
		WLog_WARN(TAG, "No file clipboard support annouonced!");
		return ERROR_BAD_ARGUMENTS;
	}

	s = Stream_New(nullptr, 4 + file_descriptor_count * CLIPRDR_FILEDESCRIPTOR_SIZE);
	if (!s)
		return ERROR_NOT_ENOUGH_MEMORY;

	Stream_Write_UINT32(s, file_descriptor_count); /* cItems (4 bytes) */

	for (UINT32 i = 0; i < file_descriptor_count; i++)
	{
		const FILEDESCRIPTORW* file = &file_descriptor_array[i];

		/*
		 * There is a known issue with Windows server getting stuck in
		 * an infinite loop when downloading files that are larger than
		 * 2 gigabytes. Do not allow clients to send such file lists.
		 *
		 * https://support.microsoft.com/en-us/help/2258090
		 */
		if ((flags & CB_HUGE_FILE_SUPPORT_ENABLED) == 0)
		{
			if ((file->nFileSizeHigh > 0) || (file->nFileSizeLow >= CLIPRDR_MAX_FILE_SIZE))
			{
				WLog_ERR(TAG, "cliprdr does not support files over 2 GB");
				result = ERROR_FILE_TOO_LARGE;
				goto error;
			}
		}

		if (!cliprdr_write_filedescriptor(s, file))
			goto error;
	}

	Stream_SealLength(s);

	*format_data = Stream_Buffer(s);
	Stream_GetLength(s, len);
	if (len > UINT32_MAX)
		goto error;

	*format_data_length = (UINT32)len;

	Stream_Free(s, FALSE);

	return result;

error:
	Stream_Free(s, TRUE);

	return result;
}
