Commit cd8eaef4 authored by Alexandre Julliard's avatar Alexandre Julliard

unicode: Avoid copying the decomposition data when not necessary.

parent a646e4e6
......@@ -4,7 +4,7 @@
#include "windef.h"
static const WCHAR table[6061] =
const WCHAR DECLSPEC_HIDDEN nfd_table[6061] =
{
/* index */
0x0110, 0x0120, 0x0130, 0x0140, 0x0150, 0x0100, 0x0160, 0x0100,
......@@ -957,17 +957,3 @@ static const WCHAR table[6061] =
0x05e9, 0x05bc, 0x05ea, 0x05bc, 0x05d5, 0x05b9, 0x05d1, 0x05bf,
0x05db, 0x05bf, 0x05e4, 0x05bf
};
unsigned int DECLSPEC_HIDDEN wine_decompose( int flags, WCHAR ch, WCHAR *dst, unsigned int dstlen )
{
unsigned short offset = table[table[ch >> 8] + ((ch >> 4) & 0xf)] + (ch & 0xf);
unsigned short start = table[offset];
unsigned short end = table[offset + 1];
unsigned int len = end - start;
*dst = ch;
if (!len) return 1;
if (dstlen < len) return 0;
memcpy( dst, table + start, len * sizeof(WCHAR) );
return len;
}
......@@ -42,11 +42,11 @@ WINE_DEFAULT_DEBUG_CHANNEL(nls);
#define CALINFO_MAX_YEAR 2029
extern UINT CDECL __wine_get_unix_codepage(void);
extern unsigned int wine_decompose( int flags, WCHAR ch, WCHAR *dst, unsigned int dstlen ) DECLSPEC_HIDDEN;
extern WCHAR wine_compose( const WCHAR *str ) DECLSPEC_HIDDEN;
extern const unsigned short wctype_table[] DECLSPEC_HIDDEN;
extern const unsigned int collation_table[] DECLSPEC_HIDDEN;
extern const unsigned short nfd_table[] DECLSPEC_HIDDEN;
static HANDLE kernel32_handle;
......@@ -675,6 +675,18 @@ static inline WCHAR casemap( const USHORT *table, WCHAR ch )
}
static const WCHAR *get_decomposition( WCHAR ch, unsigned int *len )
{
unsigned short offset = nfd_table[nfd_table[ch >> 8] + ((ch >> 4) & 0xf)] + (ch & 0xf);
unsigned short start = nfd_table[offset];
unsigned short end = nfd_table[offset + 1];
if ((*len = end - start)) return nfd_table + start;
*len = 1;
return NULL;
}
static UINT get_lcid_codepage( LCID lcid, ULONG flags )
{
UINT ret = GetACP();
......@@ -1138,15 +1150,17 @@ static int check_invalid_chars( const CPTABLEINFO *info, const unsigned char *sr
static int mbstowcs_decompose( const CPTABLEINFO *info, const unsigned char *src, int srclen,
WCHAR *dst, int dstlen )
{
WCHAR ch, dummy[4]; /* no decomposition is larger than 4 chars */
WCHAR ch;
USHORT off;
int len, res;
int len;
const WCHAR *decomp;
unsigned int decomp_len;
if (info->DBCSOffsets)
{
if (!dstlen) /* compute length */
{
for (len = 0; srclen; srclen--, src++)
for (len = 0; srclen; srclen--, src++, len += decomp_len)
{
if ((off = info->DBCSOffsets[*src]))
{
......@@ -1159,12 +1173,12 @@ static int mbstowcs_decompose( const CPTABLEINFO *info, const unsigned char *src
else ch = info->UniDefaultChar;
}
else ch = info->MultiByteTable[*src];
len += wine_decompose( 0, ch, dummy, 4 );
get_decomposition( ch, &decomp_len );
}
return len;
}
for (len = dstlen; srclen && len; srclen--, src++)
for (len = dstlen; srclen && len; srclen--, src++, dst += decomp_len, len -= decomp_len)
{
if ((off = info->DBCSOffsets[*src]))
{
......@@ -1177,25 +1191,33 @@ static int mbstowcs_decompose( const CPTABLEINFO *info, const unsigned char *src
else ch = info->UniDefaultChar;
}
else ch = info->MultiByteTable[*src];
if (!(res = wine_decompose( 0, ch, dst, len ))) break;
dst += res;
len -= res;
if ((decomp = get_decomposition( ch, &decomp_len )))
{
if (len < decomp_len) break;
memcpy( dst, decomp, decomp_len * sizeof(WCHAR) );
}
else *dst = ch;
}
}
else
{
if (!dstlen) /* compute length */
{
for (len = 0; srclen; srclen--, src++)
len += wine_decompose( 0, info->MultiByteTable[*src], dummy, 4 );
for (len = 0; srclen; srclen--, src++, len += decomp_len)
get_decomposition( info->MultiByteTable[*src], &decomp_len );
return len;
}
for (len = dstlen; srclen && len; srclen--, src++)
for (len = dstlen; srclen && len; srclen--, src++, dst += decomp_len, len -= decomp_len)
{
if (!(res = wine_decompose( 0, info->MultiByteTable[*src], dst, len ))) break;
len -= res;
dst += res;
ch = info->MultiByteTable[*src];
if ((decomp = get_decomposition( ch, &decomp_len )))
{
if (len < decomp_len) break;
memcpy( dst, decomp, decomp_len * sizeof(WCHAR) );
}
else *dst = ch;
}
}
......@@ -2227,7 +2249,7 @@ static unsigned int get_weight( WCHAR ch, enum weight type )
}
static void inc_str_pos( const WCHAR **str, int *len, int *dpos, int *dlen )
static void inc_str_pos( const WCHAR **str, int *len, unsigned int *dpos, unsigned int *dlen )
{
(*dpos)++;
if (*dpos == *dlen)
......@@ -2242,14 +2264,13 @@ static void inc_str_pos( const WCHAR **str, int *len, int *dpos, int *dlen )
static int compare_weights(int flags, const WCHAR *str1, int len1,
const WCHAR *str2, int len2, enum weight type )
{
int dpos1 = 0, dpos2 = 0, dlen1 = 0, dlen2 = 0;
WCHAR dstr1[4], dstr2[4];
unsigned int ce1, ce2;
unsigned int ce1, ce2, dpos1 = 0, dpos2 = 0, dlen1 = 0, dlen2 = 0;
const WCHAR *dstr1 = NULL, *dstr2 = NULL;
while (len1 > 0 && len2 > 0)
{
if (!dlen1) dlen1 = wine_decompose( 0, *str1, dstr1, 4 );
if (!dlen2) dlen2 = wine_decompose( 0, *str2, dstr2, 4 );
if (!dlen1 && !(dstr1 = get_decomposition( *str1, &dlen1 ))) dstr1 = str1;
if (!dlen2 && !(dstr2 = get_decomposition( *str2, &dlen2 ))) dstr2 = str2;
if (flags & NORM_IGNORESYMBOLS)
{
......@@ -2308,14 +2329,14 @@ static int compare_weights(int flags, const WCHAR *str1, int len1,
}
while (len1)
{
if (!dlen1) dlen1 = wine_decompose( 0, *str1, dstr1, 4 );
if (!dlen1 && !(dstr1 = get_decomposition( *str1, &dlen1 ))) dstr1 = str1;
ce1 = get_weight( dstr1[dpos1], type );
if (ce1) break;
inc_str_pos( &str1, &len1, &dpos1, &dlen1 );
}
while (len2)
{
if (!dlen2) dlen2 = wine_decompose( 0, *str2, dstr2, 4 );
if (!dlen2 && !(dstr2 = get_decomposition( *str2, &dlen2 ))) dstr2 = str2;
ce2 = get_weight( dstr2[dpos2], type );
if (ce2) break;
inc_str_pos( &str2, &len2, &dpos2, &dlen2 );
......
......@@ -83,8 +83,9 @@ static HMODULE kernel32_handle;
static const union cptable *unix_table; /* NULL if UTF8 */
extern WCHAR wine_compose( const WCHAR *str ) DECLSPEC_HIDDEN;
extern unsigned int wine_decompose( int flags, WCHAR ch, WCHAR *dst, unsigned int dstlen ) DECLSPEC_HIDDEN;
extern const unsigned short combining_class_table[] DECLSPEC_HIDDEN;
extern const unsigned short nfd_table[] DECLSPEC_HIDDEN;
extern const unsigned short nfkd_table[] DECLSPEC_HIDDEN;
static NTSTATUS load_string( ULONG id, LANGID lang, WCHAR *buffer, ULONG len )
{
......@@ -153,6 +154,18 @@ static WCHAR casemap_ascii( WCHAR ch )
}
static const WCHAR *get_decomposition( const unsigned short *table, WCHAR ch, unsigned int *len )
{
unsigned short offset = table[table[ch >> 8] + ((ch >> 4) & 0xf)] + (ch & 0xf);
unsigned short start = table[offset];
unsigned short end = table[offset + 1];
if ((*len = end - start)) return table + start;
*len = 1;
return NULL;
}
static BYTE get_combining_class( WCHAR c )
{
return combining_class_table[combining_class_table[combining_class_table[c >> 8] + ((c >> 4) & 0xf)] + (c & 0xf)];
......@@ -223,20 +236,27 @@ static void canonical_order_string( WCHAR *str, unsigned int len )
}
static NTSTATUS decompose_string( int flags, const WCHAR *src, int src_len, WCHAR *dst, int *dst_len )
static NTSTATUS decompose_string( int compat, const WCHAR *src, int src_len, WCHAR *dst, int *dst_len )
{
int src_pos, dst_pos = 0, decomp_len;
const unsigned short *table = compat ? nfkd_table : nfd_table;
int src_pos, dst_pos = 0;
unsigned int decomp_len;
const WCHAR *decomp;
for (src_pos = 0; src_pos < src_len; src_pos++)
{
if (dst_pos == *dst_len) break;
decomp_len = wine_decompose( flags, src[src_pos], dst + dst_pos, *dst_len - dst_pos );
if (decomp_len == 0) break;
if ((decomp = get_decomposition( table, src[src_pos], &decomp_len )))
{
if (dst_pos + decomp_len > *dst_len) break;
memcpy( dst + dst_pos, decomp, decomp_len * sizeof(WCHAR) );
}
else dst[dst_pos] = src[src_pos];
dst_pos += decomp_len;
}
if (src_pos < src_len)
{
*dst_len += (src_len - src_pos) * (flags & WINE_DECOMPOSE_COMPAT ? 18 : 3);
*dst_len += (src_len - src_pos) * (compat ? 18 : 3);
return STATUS_BUFFER_TOO_SMALL;
}
canonical_order_string( dst, dst_pos );
......@@ -1691,7 +1711,7 @@ NTSTATUS WINAPI RtlIsNormalizedString( ULONG form, const WCHAR *str, INT len, BO
*/
NTSTATUS WINAPI RtlNormalizeString( ULONG form, const WCHAR *src, INT src_len, WCHAR *dst, INT *dst_len )
{
int flags = 0, compose, compat, buf_len;
int compose, compat, buf_len;
WCHAR *buf = NULL;
NTSTATUS status = STATUS_SUCCESS;
......@@ -1721,16 +1741,14 @@ NTSTATUS WINAPI RtlNormalizeString( ULONG form, const WCHAR *src, INT src_len, W
return STATUS_SUCCESS;
}
if (compat) flags |= WINE_DECOMPOSE_COMPAT;
if (!compose) return decompose_string( flags, src, src_len, dst, dst_len );
if (!compose) return decompose_string( compat, src, src_len, dst, dst_len );
buf_len = src_len * 4;
for (;;)
{
buf = RtlAllocateHeap( GetProcessHeap(), 0, buf_len * sizeof(WCHAR) );
if (!buf) return STATUS_NO_MEMORY;
status = decompose_string( flags, src, src_len, buf, &buf_len );
status = decompose_string( compat, src, src_len, buf, &buf_len );
if (status != STATUS_BUFFER_TOO_SMALL) break;
RtlFreeHeap( GetProcessHeap(), 0, buf );
}
......
......@@ -97,9 +97,6 @@ extern int wine_compare_string( int flags, const WCHAR *str1, int len1, const WC
extern int wine_get_sortkey( int flags, const WCHAR *src, int srclen, char *dst, int dstlen );
extern int wine_fold_string( int flags, const WCHAR *src, int srclen , WCHAR *dst, int dstlen );
#define WINE_DECOMPOSE_COMPAT 1
#define WINE_DECOMPOSE_REORDER 2
extern int strcmpiW( const WCHAR *str1, const WCHAR *str2 );
extern int strncmpiW( const WCHAR *str1, const WCHAR *str2, int n );
extern int memicmpW( const WCHAR *str1, const WCHAR *str2, int n );
......
......@@ -4,7 +4,7 @@
#include "windef.h"
static const WCHAR decomp_table[6061] =
const WCHAR DECLSPEC_HIDDEN nfd_table[6061] =
{
/* index */
0x0110, 0x0120, 0x0130, 0x0140, 0x0150, 0x0100, 0x0160, 0x0100,
......@@ -958,7 +958,7 @@ static const WCHAR decomp_table[6061] =
0x05db, 0x05bf, 0x05e4, 0x05bf
};
static const WCHAR compatmap_table[13479] =
const WCHAR DECLSPEC_HIDDEN nfkd_table[13479] =
{
/* index */
0x0110, 0x0120, 0x0130, 0x0140, 0x0150, 0x0160, 0x0170, 0x0100,
......@@ -3039,20 +3039,3 @@ static const WCHAR compatmap_table[13479] =
0x00a3, 0x00ac, 0x0020, 0x0304, 0x00a6, 0x00a5, 0x20a9, 0x2502,
0x2190, 0x2191, 0x2192, 0x2193, 0x25a0, 0x25cb
};
#include "wine/unicode.h"
unsigned int DECLSPEC_HIDDEN wine_decompose( int flags, WCHAR ch, WCHAR *dst, unsigned int dstlen )
{
const WCHAR *table = (flags & WINE_DECOMPOSE_COMPAT) ? compatmap_table : decomp_table;
unsigned short offset = table[table[ch >> 8] + ((ch >> 4) & 0xf)] + (ch & 0xf);
unsigned short start = table[offset];
unsigned short end = table[offset + 1];
unsigned int len = end - start;
*dst = ch;
if (!len) return 1;
if (dstlen < len) return 0;
memcpy( dst, table + start, len * sizeof(WCHAR) );
return len;
}
......@@ -22,7 +22,18 @@
#include "wine/unicode.h"
extern unsigned int wine_decompose( int flags, WCHAR ch, WCHAR *dst, unsigned int dstlen ) DECLSPEC_HIDDEN;
extern const unsigned short nfd_table[] DECLSPEC_HIDDEN;
static const WCHAR *get_decomposition( WCHAR ch, unsigned int *len )
{
unsigned short offset = nfd_table[nfd_table[ch >> 8] + ((ch >> 4) & 0xf)] + (ch & 0xf);
unsigned short start = nfd_table[offset];
unsigned short end = nfd_table[offset + 1];
if ((*len = end - start)) return nfd_table + start;
*len = 1;
return NULL;
}
/* check the code whether it is in Unicode Private Use Area (PUA). */
/* MB_ERR_INVALID_CHARS raises an error converting from 1-byte character to PUA. */
......@@ -119,22 +130,24 @@ static int mbstowcs_sbcs_decompose( const struct sbcs_table *table, int flags,
WCHAR *dst, unsigned int dstlen )
{
const WCHAR * const cp2uni = (flags & MB_USEGLYPHCHARS) ? table->cp2uni_glyphs : table->cp2uni;
unsigned int len;
const WCHAR *decomp;
unsigned int len, decomp_len;
if (!dstlen) /* compute length */
{
WCHAR dummy[4]; /* no decomposition is larger than 4 chars */
for (len = 0; srclen; srclen--, src++)
len += wine_decompose( 0, cp2uni[*src], dummy, 4 );
for (len = 0; srclen; srclen--, src++, len += decomp_len)
get_decomposition( cp2uni[*src], &decomp_len );
return len;
}
for (len = dstlen; srclen && len; srclen--, src++)
for (len = dstlen; srclen && len; srclen--, src++, dst += decomp_len, len -= decomp_len)
{
unsigned int res = wine_decompose( 0, cp2uni[*src], dst, len );
if (!res) break;
len -= res;
dst += res;
if ((decomp = get_decomposition( cp2uni[*src], &decomp_len )))
{
if (len < decomp_len) break;
memcpy( dst, decomp, decomp_len * sizeof(WCHAR) );
}
else *dst = cp2uni[*src];
}
if (srclen) return -1; /* overflow */
return dstlen - len;
......@@ -221,13 +234,13 @@ static int mbstowcs_dbcs_decompose( const struct dbcs_table *table,
{
const WCHAR * const cp2uni = table->cp2uni;
const unsigned char * const cp2uni_lb = table->cp2uni_leadbytes;
unsigned int len, res;
const WCHAR *decomp;
unsigned int len, decomp_len;
WCHAR ch;
if (!dstlen) /* compute length */
{
WCHAR dummy[4]; /* no decomposition is larger than 4 chars */
for (len = 0; srclen; srclen--, src++)
for (len = 0; srclen; srclen--, src++, len += decomp_len)
{
unsigned char off = cp2uni_lb[*src];
if (off && srclen > 1 && src[1])
......@@ -237,12 +250,12 @@ static int mbstowcs_dbcs_decompose( const struct dbcs_table *table,
ch = cp2uni[(off << 8) + *src];
}
else ch = cp2uni[*src];
len += wine_decompose( 0, ch, dummy, 4 );
get_decomposition( ch, &decomp_len );
}
return len;
}
for (len = dstlen; srclen && len; srclen--, src++)
for (len = dstlen; srclen && len; srclen--, src++, dst += decomp_len, len -= decomp_len)
{
unsigned char off = cp2uni_lb[*src];
if (off && srclen > 1 && src[1])
......@@ -252,9 +265,13 @@ static int mbstowcs_dbcs_decompose( const struct dbcs_table *table,
ch = cp2uni[(off << 8) + *src];
}
else ch = cp2uni[*src];
if (!(res = wine_decompose( 0, ch, dst, len ))) break;
dst += res;
len -= res;
if ((decomp = get_decomposition( ch, &decomp_len )))
{
if (len < decomp_len) break;
memcpy( dst, decomp, decomp_len * sizeof(WCHAR) );
}
else *dst = ch;
}
if (srclen) return -1; /* overflow */
return dstlen - len;
......
......@@ -19,8 +19,19 @@
*/
#include "wine/unicode.h"
extern unsigned int wine_decompose( int flags, WCHAR ch, WCHAR *dst, unsigned int dstlen );
extern const unsigned int collation_table[];
extern const unsigned short nfd_table[] DECLSPEC_HIDDEN;
static const WCHAR *get_decomposition( WCHAR ch, unsigned int *len )
{
unsigned short offset = nfd_table[nfd_table[ch >> 8] + ((ch >> 4) & 0xf)] + (ch & 0xf);
unsigned short start = nfd_table[offset];
unsigned short end = nfd_table[offset + 1];
if ((*len = end - start)) return nfd_table + start;
*len = 1;
return NULL;
}
/*
* flags - normalization NORM_* flags
......@@ -180,7 +191,7 @@ static unsigned int get_weight(WCHAR ch, enum weight type)
}
}
static void inc_str_pos(const WCHAR **str, int *len, int *dpos, int *dlen)
static void inc_str_pos(const WCHAR **str, int *len, unsigned int *dpos, unsigned int *dlen)
{
(*dpos)++;
if (*dpos == *dlen)
......@@ -194,9 +205,8 @@ static void inc_str_pos(const WCHAR **str, int *len, int *dpos, int *dlen)
static inline int compare_weights(int flags, const WCHAR *str1, int len1,
const WCHAR *str2, int len2, enum weight type)
{
int dpos1 = 0, dpos2 = 0, dlen1 = 0, dlen2 = 0;
WCHAR dstr1[4], dstr2[4];
unsigned int ce1, ce2;
unsigned int ce1, ce2, dpos1 = 0, dpos2 = 0, dlen1 = 0, dlen2 = 0;
const WCHAR *dstr1 = NULL, *dstr2 = NULL;
/* 32-bit collation element table format:
* unicode weight - high 16 bit, diacritic weight - high 8 bit of low 16 bit,
......@@ -204,8 +214,8 @@ static inline int compare_weights(int flags, const WCHAR *str1, int len1,
*/
while (len1 > 0 && len2 > 0)
{
if (!dlen1) dlen1 = wine_decompose(0, *str1, dstr1, 4);
if (!dlen2) dlen2 = wine_decompose(0, *str2, dstr2, 4);
if (!dlen1 && !(dstr1 = get_decomposition( *str1, &dlen1 ))) dstr1 = str1;
if (!dlen2 && !(dstr2 = get_decomposition( *str2, &dlen2 ))) dstr2 = str2;
if (flags & NORM_IGNORESYMBOLS)
{
......@@ -264,16 +274,14 @@ static inline int compare_weights(int flags, const WCHAR *str1, int len1,
}
while (len1)
{
if (!dlen1) dlen1 = wine_decompose(0, *str1, dstr1, 4);
if (!dlen1 && !(dstr1 = get_decomposition( *str1, &dlen1 ))) dstr1 = str1;
ce1 = get_weight(dstr1[dpos1], type);
if (ce1) break;
inc_str_pos(&str1, &len1, &dpos1, &dlen1);
}
while (len2)
{
if (!dlen2) dlen2 = wine_decompose(0, *str2, dstr2, 4);
if (!dlen2 && !(dstr2 = get_decomposition( *str2, &dlen2 ))) dstr2 = str2;
ce2 = get_weight(dstr2[dpos2], type);
if (ce2) break;
inc_str_pos(&str2, &len2, &dpos2, &dlen2);
......
......@@ -2380,8 +2380,6 @@ sub dump_decompositions($@)
{
my ($name, @decomp) = @_;
@decomp = build_decompositions( @decomp );
# first determine all the 16-char subsets that contain something
my @filled = (0) x 4096;
......@@ -2423,7 +2421,7 @@ sub dump_decompositions($@)
# dump the main index
printf OUTPUT "static const WCHAR %s[%d] =\n", $name, $total + $data_total;
printf OUTPUT "\nconst WCHAR DECLSPEC_HIDDEN %s[%d] =\n", $name, $total + $data_total;
printf OUTPUT "{\n /* index */\n";
printf OUTPUT "%s", dump_array( 16, 0, @filled_idx );
printf OUTPUT ",\n /* null sub-index */\n%s", dump_array( 16, 0, ($null_offset) x 16 );
......@@ -2471,7 +2469,7 @@ sub dump_decompositions($@)
printf OUTPUT ",\n /* data */\n";
printf OUTPUT "%s", dump_array( 16, 0, @data );
printf OUTPUT "\n};\n\n";
printf OUTPUT "\n};\n";
}
################################################################
......@@ -2485,50 +2483,11 @@ sub dump_decompose_table($$)
print OUTPUT "/* Unicode char composition */\n";
print OUTPUT "/* generated from $UNIDATA/UnicodeData.txt */\n";
print OUTPUT "/* DO NOT EDIT!! */\n\n";
print OUTPUT "#include \"windef.h\"\n\n";
dump_decompositions( $compat ? "decomp_table" : "table", @decomp_table );
print OUTPUT "#include \"windef.h\"\n";
if ($compat)
{
dump_decompositions( "compatmap_table", @decomp_compat_table );
print OUTPUT <<"EOF";
#include "wine/unicode.h"
dump_decompositions( "nfd_table", build_decompositions( @decomp_table ));
dump_decompositions( "nfkd_table", build_decompositions( @decomp_compat_table )) if $compat;
unsigned int DECLSPEC_HIDDEN wine_decompose( int flags, WCHAR ch, WCHAR *dst, unsigned int dstlen )
{
const WCHAR *table = (flags & WINE_DECOMPOSE_COMPAT) ? compatmap_table : decomp_table;
unsigned short offset = table[table[ch >> 8] + ((ch >> 4) & 0xf)] + (ch & 0xf);
unsigned short start = table[offset];
unsigned short end = table[offset + 1];
unsigned int len = end - start;
*dst = ch;
if (!len) return 1;
if (dstlen < len) return 0;
memcpy( dst, table + start, len * sizeof(WCHAR) );
return len;
}
EOF
}
else
{
print OUTPUT <<"EOF";
unsigned int DECLSPEC_HIDDEN wine_decompose( int flags, WCHAR ch, WCHAR *dst, unsigned int dstlen )
{
unsigned short offset = table[table[ch >> 8] + ((ch >> 4) & 0xf)] + (ch & 0xf);
unsigned short start = table[offset];
unsigned short end = table[offset + 1];
unsigned int len = end - start;
*dst = ch;
if (!len) return 1;
if (dstlen < len) return 0;
memcpy( dst, table + start, len * sizeof(WCHAR) );
return len;
}
EOF
}
close OUTPUT;
save_file($filename);
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment