utf8 aware strncpy

kcrjzv8t  于 2023-03-28  发布在  其他
关注(0)|答案(6)|浏览(142)

我很难相信我是第一个遇到这个问题的人,但我找了很长时间,没有找到解决这个问题的方法。
我想使用strncpy,但它是UTF8感知的,所以它不会部分地将utf8代码点写入目标字符串。
否则,您永远无法确定结果字符串是有效的UTF8,即使您知道源是(当源字符串大于最大长度时)。
验证结果字符串可以工作,但如果这是要调用很多,最好有一个strncpy函数来检查它。
glib有g_utf8_strncpy,但它复制了一定数量的unicode字符,而我正在寻找一个受字节长度限制的复制函数。
需要明确的是,“utf8 aware”,我的意思是它不应该超过目标缓冲区的限制,并且它必须永远只复制utf-8代码点的一部分。(给定有效的utf-8输入,永远不会导致无效的utf-8输出)。

注:

一些回复指出,strncpy空所有字节,它不会确保零终止,回想起来,我应该要求一个utf8感知**strlcpy**,但当时我不知道这个函数的存在。

ezykj2lf

ezykj2lf1#

我已经在许多包含多字节字符的UTF8示例字符串上测试了这个方法。如果源代码太长,它会反向搜索它(从null终止符开始),并向后查找最后一个可以放入目标缓冲区的完整UTF8字符。它总是确保目标是null终止的。

char* utf8cpy(char* dst, const char* src, size_t sizeDest )
{
    if( sizeDest ){
        size_t sizeSrc = strlen(src); // number of bytes not including null
        while( sizeSrc >= sizeDest ){

            const char* lastByte = src + sizeSrc; // Initially, pointing to the null terminator.
            while( lastByte-- > src )
                if((*lastByte & 0xC0) != 0x80) // Found the initial byte of the (potentially) multi-byte character (or found null).
                    break;

            sizeSrc = lastByte - src;
        }
        memcpy(dst, src, sizeSrc);
        dst[sizeSrc] = '\0';
    }
    return dst;
}
mwecs4sa

mwecs4sa2#

我不知道你说的UTF-8感知是什么意思;strncpy复制字节,而不是字符,缓冲区的大小也是以字节为单位的。如果你的意思是它只复制完整的UTF-8字符,例如,如果没有下一个字符的空间,就停止,我不知道这样的函数,但它应该不会太难写:

int
utf8Size( char ch )
{
    static int const sizeTable[] =
    {
        //  ...
    };
    return sizeTable( static_cast<unsigned char>( ch ) )
}

char*
stru8ncpy( char* dest, char* source, int n )
{
    while ( *source != '\0' && utf8Size( *source ) < n ) {
        n -= utf8Size( *source );
        switch ( utf8Size( ch ) ) {
        case 6:
            *dest ++ = *source ++;
        case 5:
            *dest ++ = *source ++;
        case 4:
            *dest ++ = *source ++;
        case 3:
            *dest ++ = *source ++;
        case 2:
            *dest ++ = *source ++;
        case 1:
            *dest ++ = *source ++;
            break;
        default:
            throw IllegalUTF8();
        }
    }
    *dest = '\0';
    return dest;
}

(The在utf8 Size中生成表的内容有点麻烦,但是如果你处理UTF-8,这是一个你会经常使用的函数,而且你只需要做一次。

2izufjch

2izufjch3#

为了回答自己的问题,这里的C函数我结束了(不使用C为这个项目):
注意:-要知道这不是utf8的strncpy的克隆,它更像是来自openbsd的strlcpy。- utf8_skip_data从glib的gutf8.c复制-它不验证utf8 -这是我的意图。
希望这对其他人有用,并对反馈感兴趣,但请不要对NULL终止行为学究气的狂热者,除非它是一个实际的错误,或误导/不正确的行为。
感谢James Kanze,他提供了这方面的基础,但不完整和C
(我需要一个C版本)。

static const size_t utf8_skip_data[256] = {
    1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
    1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
    1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
    1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
    1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
    1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
    2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,
    3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,4,4,4,4,4,4,4,4,5,5,5,5,6,6,1,1
};

char *strlcpy_utf8(char *dst, const char *src, size_t maxncpy)
{
    char *dst_r = dst;
    size_t utf8_size;

    if (maxncpy > 0) {
        while (*src != '\0' && (utf8_size = utf8_skip_data[*((unsigned char *)src)]) < maxncpy) {
            maxncpy -= utf8_size;
            switch (utf8_size) {
                case 6: *dst ++ = *src ++;
                case 5: *dst ++ = *src ++;
                case 4: *dst ++ = *src ++;
                case 3: *dst ++ = *src ++;
                case 2: *dst ++ = *src ++;
                case 1: *dst ++ = *src ++;
            }
        }
        *dst= '\0';
    }
    return dst_r;
}
0dxa2lsx

0dxa2lsx4#

strncpy()是一个很糟糕的函数:
1.如果没有足够的空间,结果字符串将不以nul结尾
1.如果有足够的空间,剩余部分将用NULL填充。如果目标字符串非常大,这可能会很痛苦。
即使字符保持在ASCII范围内(0x 7 f及以下),结果字符串也不会是你想要的。在UTF-8的情况下,它可能不是以null结尾的和以无效的UTF-8序列结尾的
最好的建议是避免strncpy()

**编辑:**ad 1):

#include <stdio.h>
#include <string.h>

int main (void)
{
char buff [4];

strncpy (buff, "hello world!\n", sizeof buff );
printf("%s\n", buff );

return 0;
}

同意,缓冲区不会溢出。但结果仍然是不需要的。strncpy()只解决了问题的一部分。它是误导和不需要的。
更新(2012-10-31):由于这是一个令人讨厌的问题,我决定破解我自己的版本,模仿丑陋的strncpy()行为。

#include <stdio.h>
#include <string.h>

size_t utf8ncpy(char *dst, char *src, size_t todo);
static int cnt_utf8(unsigned ch, size_t len);

static int cnt_utf8(unsigned ch, size_t len)
{
if (!len) return 0;

if ((ch & 0x80) == 0x00) return 1;
else if ((ch & 0xe0) == 0xc0) return 2;
else if ((ch & 0xf0) == 0xe0) return 3;
else if ((ch & 0xf8) == 0xf0) return 4;
else if ((ch & 0xfc) == 0xf8) return 5;
else if ((ch & 0xfe) == 0xfc) return 6;
else return -1; /* Default (Not in the spec) */
}

size_t utf8ncpy(char *dst, char *src, size_t todo)
{
size_t done, idx, chunk, srclen;

srclen = strlen(src);
for(done=idx=0; idx < srclen; idx+=chunk) {
        int ret;
        for (chunk=0; done+chunk < todo; chunk++) {
                ret = cnt_utf8( src[idx+chunk], srclen - (idx+chunk) );
                if (ret ==1) continue;  /* Normal character: collect it into chunk */
                if (ret < 0) continue;  /* Bad stuff: treat as normal char */
                if (ret ==0) break;     /* EOF */
                if (!chunk) chunk = ret;/* an UTF8 multibyte character */
                else ret = 1;           /* we allready collected a number (chunk) of normal characters */
                break;
                }
        if (ret > 1 && done+chunk > todo) break;
        if (done+chunk > todo) chunk = todo - done;
        if (!chunk) break;
        memcpy( dst+done, src+idx, chunk);
        done += chunk;
        if (ret < 1) break;
        }
        /* This is part of the dreaded strncpy() behavior:
        ** pad the destination string with NULs
        ** upto its intended size
        */
if (done < todo) memset(dst+done, 0, todo-done);
return done;
}

int main(void)
{
char *string = "Hell\xc3\xb6 \xf1\x82\x82\x82, world\xc2\xa1!";
char buffer[30];
unsigned result, len;

for (len = sizeof buffer-1; len < sizeof buffer; len -=3) {
        result = utf8ncpy(buffer, string, len);
        /* remove the following line to get the REAL strncpy() behaviour */
        buffer[result] = 0;
        printf("Chop @%u\n", len );
        printf("Org:[%s]\n", string );
        printf("Res:%u\n", result );
        printf("New:[%s]\n", buffer );
        }

return 0;
}
x7rlezfr

x7rlezfr5#

下面是一个C++解决方案:
u8string.h

#ifndef U8STRING_H
#define U8STRING_H 1
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif

/**
 * Copies the first few characters of the UTF-8-encoded string pointed to by
 * \p src into \p dest_buf, as many UTF-8-encoded characters as can be written in
 * <code>dest_buf_len - 1</code> bytes or until the NUL terminator of the string
 * pointed to by \p str is reached.
 *
 * The string of bytes that are written into \p dest_buf is NUL terminated
 * if \p dest_buf_len is greater than 0.
 *
 * \returns \p dest_buf
 */
char * u8slbcpy(char *dest_buf, const char *src, size_t dest_buf_len);

#ifdef __cplusplus
}
#endif
#endif

u8slbcpy.cpp

#include "u8string.h"

#include <cstring>
#include <utf8.h>

char * u8slbcpy(char *dest_buf, const char *src, size_t dest_buf_len)
{
    if (dest_buf_len <= 0) {
        return dest_buf;
    } else if (dest_buf_len == 1) {
        dest_buf[0] = '\0';
        return dest_buf;
    }

    size_t num_bytes_remaining = dest_buf_len - 1;
    utf8::unchecked::iterator<const char *> it(src);
    const char * prev_base = src;
    while (*it++ != '\0') {
        const char *base = it.base();
        ptrdiff_t diff = (base - prev_base);
        if (num_bytes_remaining < diff) {
            break;
        }
        num_bytes_remaining -= diff;
        prev_base = base;
    }

    size_t n = dest_buf_len - 1 - num_bytes_remaining;
    std::memmove(dest_buf, src, n);
    dest_buf[n] = '\0';

    return dest_buf;
}

函数u8slbcpy()有一个C接口,但它是用C++实现的。我的实现使用的是仅头部的UTF8-CPP library
我想这差不多就是您要找的东西,但是请注意,如果组合字符应用于第 n 个字符,则仍然存在一个或多个组合字符可能无法复制的问题(本身不是组合字符),并且目的地缓冲区刚好足够大以存储字符1到 n 的UTF-8编码,但不写入字符 n 的组合字符。在这种情况下,写入了表示字符1到 n 的字节,但没有写入 n 的组合字符。实际上,可以说第 n 个字符被部分写入。

yyyllmsg

yyyllmsg6#

评论上面的答案“strncpy()是一个可怕的函数:“.我讨厌甚至评论这样的毯子声明在创建另一个互联网编程圣战的代价,但无论如何,因为像这样的声明是误导那些可能来这里寻找答案.
好吧,也许C字符串函数是“老派”的。也许C/C中的所有字符串都应该在某种智能容器中,等等,也许应该使用C而不是C(当你有选择的时候),这些更多的是一种偏好和其他主题的争论。
我来这里寻找一个UTF-8 strncpy()我自己。不是说我不能做一个(编码是IMHO简单和优雅),但想看看别人是如何使他们的,也许找到一个优化的ASM之一。
对于编程世界的人们来说,把你的傲慢放在一边,看看一些事实。
“strncpy()"或任何其他类似的函数都没有问题,它们具有相同的副作用和问题,如“_snprintf()"等。
我说:“strncpy()并不可怕”,而是“可怕的程序员使用它非常糟糕”。
什么是“可怕的”是不知道的规则.此外,在整个主题,因为安全(如缓冲区溢出)和程序稳定性的影响,就不会有一个需要,例如微软添加到它的CRT库“安全字符串函数”,如果只是遵循规则.
主要的:
1.“sizeof()”返回带有终止符的静态字符串的长度。
1.“strlen()”返回字符串的长度,不带终止符。
1.大多数(如果没有的话)“n”函数只是箝位到“n”,而不添加终止符。
1.在需要和输入缓冲区大小的函数中,“缓冲区大小”是什么是隐含的模糊性。即“(char *pszBuffer,int iBufferSize)”类型。更安全的是假设最坏的情况,并传递比实际缓冲区大小小一的大小,并在末尾添加终止符以确保安全。
1.对于字符串输入,缓冲区等,根据预期的平均值和最大值设置和使用合理的大小限制。希望避免输入截断,并消除缓冲区溢出周期。
这是我个人处理这类事情的方式,还有其他规则,只是要知道和实践。
一个方便的静态字符串大小宏:

// Size of a string with out terminator
#define SIZESTR(x) (sizeof(x) - 1)

声明本地/堆栈字符串缓冲区时:
A)例如,将终止符的大小限制为1023+1,以允许字符串的长度达到1023个字符。
B)我将字符串的尺子初始化为零,并在最后终止以覆盖可能的'n'截断。

char szBuffer[1024]; szBuffer[0] = szBuffer[SIZESTR(szBuffer)] = 0;

或者,可以只执行以下操作:char szBuffer[1024] = {0};,但是对于编译器生成的memset()来说有一些性能暗示,比如调用整个缓冲区为零。这使得调试更干净,我更喜欢静态(相对于本地/堆栈)字符串缓冲区的这种风格。
现在一个“strncpy()”遵循以下规则:

char szBuffer[1024]; szBuffer[0] = szBuffer[SIZESTR(szBuffer)] = 0; 
strncpy(szBuffer, pszSomeInput, SIZESTR(szBuffer));

当然还有其他的“规则”和问题,但这些是最主要的,你只需要知道lib函数是如何工作的,并使用这样的安全实践。
最后,在我的项目中,无论如何我都使用ICU,所以我决定使用它并使用“utf8.h”中的宏来创建我自己的“strncpy()”。

相关问题