/*
 *  Copyright (C) 2012-2013 Samsung Electronics Co., Ltd.
 *
 *  This program is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU General Public License
 *  as published by the Free Software Foundation; either version 2
 *  of the License, or (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, see <http://www.gnu.org/licenses/>.
 */

/************************************************************************/
/*                                                                      */
/*  PROJECT : exFAT & FAT12/16/32 File System                           */
/*  FILE    : nls.c                                                     */
/*  PURPOSE : sdFAT NLS Manager                                         */
/*                                                                      */
/*----------------------------------------------------------------------*/
/*  NOTES                                                               */
/*                                                                      */
/*                                                                      */
/************************************************************************/
#include <linux/string.h>
#include <linux/nls.h>

#include "sdfat.h"
#include "core.h"

/*----------------------------------------------------------------------*/
/*  Global Variable Definitions                                         */
/*----------------------------------------------------------------------*/

/*----------------------------------------------------------------------*/
/*  Local Variable Definitions                                          */
/*----------------------------------------------------------------------*/

static u16 bad_dos_chars[] = {
	/* + , ; = [ ] */
	0x002B, 0x002C, 0x003B, 0x003D, 0x005B, 0x005D,
	0xFF0B, 0xFF0C, 0xFF1B, 0xFF1D, 0xFF3B, 0xFF3D,
	0
};

/*
 * Allow full-width illegal characters :
 * "MS windows 7" supports full-width-invalid-name-characters.
 * So we should check half-width-invalid-name-characters(ASCII) only
 * for compatibility.
 *
 * " * / : < > ? \ |
 *
 * patch 1.2.0
 */
static u16 bad_uni_chars[] = {
	0x0022,         0x002A, 0x002F, 0x003A,
	0x003C, 0x003E, 0x003F, 0x005C, 0x007C,
#if 0 /* allow full-width characters */
	0x201C, 0x201D, 0xFF0A, 0xFF0F, 0xFF1A,
	0xFF1C, 0xFF1E, 0xFF1F, 0xFF3C, 0xFF5C,
#endif
	0
};

/*----------------------------------------------------------------------*/
/*  Local Function Declarations                                         */
/*----------------------------------------------------------------------*/
static s32  convert_uni_to_ch(struct nls_table *nls, u16 uni, u8 *ch, s32 *lossy);
static s32  convert_ch_to_uni(struct nls_table *nls, u8 *ch, u16 *uni, s32 *lossy);

static u16 nls_upper(struct super_block *sb, u16 a)
{
	FS_INFO_T *fsi = &(SDFAT_SB(sb)->fsi);

	if (SDFAT_SB(sb)->options.casesensitive)
		return a;
	if ((fsi->vol_utbl)[get_col_index(a)] != NULL)
		return (fsi->vol_utbl)[get_col_index(a)][get_row_index(a)];
	else
		return a;
}
/*======================================================================*/
/*  Global Function Definitions                                         */
/*======================================================================*/
u16 *nls_wstrchr(u16 *str, u16 wchar)
{
	while (*str) {
		if (*(str++) == wchar)
			return str;
	}

	return 0;
}

s32 nls_cmp_sfn(struct super_block *sb, u8 *a, u8 *b)
{
	return strncmp((void *)a, (void *)b, DOS_NAME_LENGTH);
}

s32 nls_cmp_uniname(struct super_block *sb, u16 *a, u16 *b)
{
	s32 i;

	for (i = 0; i < MAX_NAME_LENGTH; i++, a++, b++) {
		if (nls_upper(sb, *a) != nls_upper(sb, *b))
			return 1;
		if (*a == 0x0)
			return 0;
	}
	return 0;
}

#define CASE_LOWER_BASE (0x08)	/* base is lower case */
#define CASE_LOWER_EXT  (0x10)	/* extension is lower case */

s32 nls_uni16s_to_sfn(struct super_block *sb, UNI_NAME_T *p_uniname, DOS_NAME_T *p_dosname, s32 *p_lossy)
{
	s32 i, j, len, lossy = NLS_NAME_NO_LOSSY;
	u8 buf[MAX_CHARSET_SIZE];
	u8 lower = 0, upper = 0;
	u8 *dosname = p_dosname->name;
	u16 *uniname = p_uniname->name;
	u16 *p, *last_period;
	struct nls_table *nls = SDFAT_SB(sb)->nls_disk;

	/* DOSNAME is filled with space */
	for (i = 0; i < DOS_NAME_LENGTH; i++)
		*(dosname+i) = ' ';

	/* DOT and DOTDOT are handled by VFS layer */

	/* search for the last embedded period */
	last_period = NULL;
	for (p = uniname; *p; p++) {
		if (*p == (u16) '.')
			last_period = p;
	}

	i = 0;
	while (i < DOS_NAME_LENGTH) {
		if (i == 8) {
			if (last_period == NULL)
				break;

			if (uniname <= last_period) {
				if (uniname < last_period)
					lossy |= NLS_NAME_OVERLEN;
				uniname = last_period + 1;
			}
		}

		if (*uniname == (u16) '\0') {
			break;
		} else if (*uniname == (u16) ' ') {
			lossy |= NLS_NAME_LOSSY;
		} else if (*uniname == (u16) '.') {
			if (uniname < last_period)
				lossy |= NLS_NAME_LOSSY;
			else
				i = 8;
		} else if (nls_wstrchr(bad_dos_chars, *uniname)) {
			lossy |= NLS_NAME_LOSSY;
			*(dosname+i) = '_';
			i++;
		} else {
			len = convert_uni_to_ch(nls, *uniname, buf, &lossy);

			if (len > 1) {
				if ((i >= 8) && ((i+len) > DOS_NAME_LENGTH))
					break;

				if ((i <  8) && ((i+len) > 8)) {
					i = 8;
					continue;
				}

				lower = 0xFF;

				for (j = 0; j < len; j++, i++)
					*(dosname+i) = *(buf+j);
			} else { /* len == 1 */
				if ((*buf >= 'a') && (*buf <= 'z')) {
					*(dosname+i) = *buf - ('a' - 'A');

					lower |= (i < 8) ?
						CASE_LOWER_BASE :
						CASE_LOWER_EXT;
				} else if ((*buf >= 'A') && (*buf <= 'Z')) {
					*(dosname+i) = *buf;

					upper |= (i < 8) ?
						CASE_LOWER_BASE :
						CASE_LOWER_EXT;
				} else {
					*(dosname+i) = *buf;
				}
				i++;
			}
		}

		uniname++;
	}

	if (*dosname == 0xE5)
		*dosname = 0x05;
	if (*uniname != 0x0)
		lossy |= NLS_NAME_OVERLEN;

	if (upper & lower)
		p_dosname->name_case = 0xFF;
	else
		p_dosname->name_case = lower;

	if (p_lossy)
		*p_lossy = lossy;
	return i;
}

s32 nls_sfn_to_uni16s(struct super_block *sb, DOS_NAME_T *p_dosname, UNI_NAME_T *p_uniname)
{
	s32 i = 0, j, n = 0;
	u8 buf[MAX_DOSNAME_BUF_SIZE];
	u8 *dosname = p_dosname->name;
	u16 *uniname = p_uniname->name;
	struct nls_table *nls = SDFAT_SB(sb)->nls_disk;

	if (*dosname == 0x05) {
		*buf = 0xE5;
		i++;
		n++;
	}

	for ( ; i < 8; i++, n++) {
		if (*(dosname+i) == ' ')
			break;

		if ((*(dosname+i) >= 'A') && (*(dosname+i) <= 'Z') &&
				(p_dosname->name_case & CASE_LOWER_BASE))
			*(buf+n) = *(dosname+i) + ('a' - 'A');
		else
			*(buf+n) = *(dosname+i);
	}
	if (*(dosname+8) != ' ') {
		*(buf+n) = '.';
		n++;
	}

	for (i = 8; i < DOS_NAME_LENGTH; i++, n++) {
		if (*(dosname+i) == ' ')
			break;

		if ((*(dosname+i) >= 'A') && (*(dosname+i) <= 'Z') &&
			       (p_dosname->name_case & CASE_LOWER_EXT))
			*(buf+n) = *(dosname+i) + ('a' - 'A');
		else
			*(buf+n) = *(dosname+i);
	}
	*(buf+n) = '\0';

	i = j = 0;
	while (j < MAX_NAME_LENGTH) {
		if (*(buf+i) == '\0')
			break;

		i += convert_ch_to_uni(nls, (buf+i), uniname, NULL);

		uniname++;
		j++;
	}

	*uniname = (u16) '\0';
	return j;
}

static s32 __nls_utf16s_to_vfsname(struct super_block *sb, UNI_NAME_T *p_uniname, u8 *p_cstring, s32 buflen)
{
	s32 len;
	const u16 *uniname = p_uniname->name;

	/* always len >= 0 */
	len = utf16s_to_utf8s(uniname, MAX_NAME_LENGTH, UTF16_HOST_ENDIAN,
		p_cstring, buflen);
	p_cstring[len] = '\0';
	return len;
}

static s32 __nls_vfsname_to_utf16s(struct super_block *sb, const u8 *p_cstring,
		const s32 len, UNI_NAME_T *p_uniname, s32 *p_lossy)
{
	s32 i, unilen, lossy = NLS_NAME_NO_LOSSY;
	u16 upname[MAX_NAME_LENGTH+1];
	u16 *uniname = p_uniname->name;

	BUG_ON(!len);

	unilen = utf8s_to_utf16s(p_cstring, len, UTF16_HOST_ENDIAN,
			(wchar_t *)uniname, MAX_NAME_LENGTH+2);
	if (unilen < 0) {
		MMSG("%s: failed to vfsname_to_utf16(err:%d) "
			"vfsnamelen:%d", __func__, unilen, len);
		return unilen;
	}

	if (unilen > MAX_NAME_LENGTH) {
		MMSG("%s: failed to vfsname_to_utf16(estr:ENAMETOOLONG) "
			"vfsnamelen:%d, unilen:%d>%d",
			__func__, len, unilen, MAX_NAME_LENGTH);
		return -ENAMETOOLONG;
	}

	p_uniname->name_len = (u8)(unilen & 0xFF);

	for (i = 0; i < unilen; i++) {
		if ((*uniname < 0x0020) || nls_wstrchr(bad_uni_chars, *uniname))
			lossy |= NLS_NAME_LOSSY;

		*(upname+i) = nls_upper(sb, *uniname);
		uniname++;
	}

	*uniname = (u16)'\0';
	p_uniname->name_len = unilen;
	p_uniname->name_hash = calc_chksum_2byte((void *) upname,
				unilen << 1, 0, CS_DEFAULT);

	if (p_lossy)
		*p_lossy = lossy;

	return unilen;
}

static s32 __nls_uni16s_to_vfsname(struct super_block *sb, UNI_NAME_T *p_uniname, u8 *p_cstring, s32 buflen)
{
	s32 i, j, len, out_len = 0;
	u8 buf[MAX_CHARSET_SIZE];
	const u16 *uniname = p_uniname->name;
	struct nls_table *nls = SDFAT_SB(sb)->nls_io;

	i = 0;
	while ((i < MAX_NAME_LENGTH) && (out_len < (buflen-1))) {
		if (*uniname == (u16)'\0')
			break;

		len = convert_uni_to_ch(nls, *uniname, buf, NULL);

		if (out_len + len >= buflen)
			len = (buflen - 1) - out_len;

		out_len += len;

		if (len > 1) {
			for (j = 0; j < len; j++)
				*p_cstring++ = (s8) *(buf+j);
		} else { /* len == 1 */
			*p_cstring++ = (s8) *buf;
		}

		uniname++;
		i++;
	}

	*p_cstring = '\0';
	return out_len;
}

static s32 __nls_vfsname_to_uni16s(struct super_block *sb, const u8 *p_cstring,
		const s32 len, UNI_NAME_T *p_uniname, s32 *p_lossy)
{
	s32 i, unilen, lossy = NLS_NAME_NO_LOSSY;
	u16 upname[MAX_NAME_LENGTH+1];
	u16 *uniname = p_uniname->name;
	struct nls_table *nls = SDFAT_SB(sb)->nls_io;

	BUG_ON(!len);

	i = unilen = 0;
	while ((unilen < MAX_NAME_LENGTH) && (i < len)) {
		i += convert_ch_to_uni(nls, (u8 *)(p_cstring+i), uniname, &lossy);

		if ((*uniname < 0x0020) || nls_wstrchr(bad_uni_chars, *uniname))
			lossy |= NLS_NAME_LOSSY;

		*(upname+unilen) = nls_upper(sb, *uniname);

		uniname++;
		unilen++;
	}

	if (*(p_cstring+i) != '\0')
		lossy |= NLS_NAME_OVERLEN;

	*uniname = (u16)'\0';
	p_uniname->name_len = unilen;
	p_uniname->name_hash =
		calc_chksum_2byte((void *) upname, unilen<<1, 0, CS_DEFAULT);

	if (p_lossy)
		*p_lossy = lossy;

	return unilen;
}

s32 nls_uni16s_to_vfsname(struct super_block *sb, UNI_NAME_T *uniname, u8 *p_cstring, s32 buflen)
{
	if (SDFAT_SB(sb)->options.utf8)
		return __nls_utf16s_to_vfsname(sb, uniname, p_cstring, buflen);

	return __nls_uni16s_to_vfsname(sb, uniname, p_cstring, buflen);
}

s32 nls_vfsname_to_uni16s(struct super_block *sb, const u8 *p_cstring, const s32 len, UNI_NAME_T *uniname, s32 *p_lossy)
{
	if (SDFAT_SB(sb)->options.utf8)
		return __nls_vfsname_to_utf16s(sb, p_cstring, len, uniname, p_lossy);
	return __nls_vfsname_to_uni16s(sb, p_cstring, len, uniname, p_lossy);
}

/*======================================================================*/
/*  Local Function Definitions                                          */
/*======================================================================*/

static s32 convert_ch_to_uni(struct nls_table *nls, u8 *ch, u16 *uni, s32 *lossy)
{
	int len;

	*uni = 0x0;

	if (ch[0] < 0x80) {
		*uni = (u16) ch[0];
		return 1;
	}

	len = nls->char2uni(ch, MAX_CHARSET_SIZE, uni);
	if (len < 0) {
		/* conversion failed */
		DMSG("%s: fail to use nls\n", __func__);
		if (lossy != NULL)
			*lossy |= NLS_NAME_LOSSY;
		*uni = (u16) '_';
		if (!strcmp(nls->charset, "utf8"))
			return 1;
		return 2;
	}

	return len;
} /* end of convert_ch_to_uni */

static s32 convert_uni_to_ch(struct nls_table *nls, u16 uni, u8 *ch, s32 *lossy)
{
	int len;

	ch[0] = 0x0;

	if (uni < 0x0080) {
		ch[0] = (u8) uni;
		return 1;
	}

	len = nls->uni2char(uni, ch, MAX_CHARSET_SIZE);
	if (len < 0) {
		/* conversion failed */
		DMSG("%s: fail to use nls\n", __func__);
		if (lossy != NULL)
			*lossy |= NLS_NAME_LOSSY;
		ch[0] = '_';
		return 1;
	}

	return len;

} /* end of convert_uni_to_ch */

/* end of nls.c */