【问题标题】:utf8 aware strncpyUTF8 感知 strncpy
【发布时间】:2011-11-12 18:24:51
【问题描述】:

我很难相信我是第一个遇到这个问题的人,但我搜索了很长时间,但没有找到解决方案。

我想使用 strncpy,但让它支持 UTF8,这样它就不会将 utf8 字符部分写入目标字符串。

否则,您永远无法确定生成的字符串是有效的 UTF8,即使您知道源是(当源字符串大于最大长度时)。

验证结果字符串可以工作,但如果要经常调用它,最好有一个 strncpy 函数来检查它。

glib 有 g_utf8_strncpy 但这会复制一定数量的 unicode 字符,而我正在寻找一个受字节长度限制的复制函数。

明确地说,“utf8 感知” 是指它不应超过目标缓冲区的限制,并且它必须从不仅复制 utf 的一部分-8 个字符。 (给定有效的 utf-8 输入决不能导致无效的 utf-8 输出)。


注意:

一些回复指出strncpy 将所有字节都归零,并且它不会确保零终止,回想起来我应该要求一个支持 utf8 的 strlcpy,但是当时我没有'不知道这个函数的存在。

【问题讨论】:

  • 通常你使用完全支持 UTF-8 的库,比如 ICU icu-project.org 来解决这些问题,最后,谁保证一个 char* 是一个 UTF-8 字符串,而不是随机垃圾 null 终止?
  • 那又怎样? strncpy 也不保证会产生一个以零结尾的 C 字符串。与广泛传播的看法相反,strncpy 不是“字符串”函数,而是缓冲区处理函数。它的两个经常被遗忘的副作用提供了一个线索(它的第二个副作用是给定大小的缓冲区归零)。
  • @Zan Lynx,无法调整大小的目标字符串,整个 API/结构等都依赖于固定字符串。
  • @tristopia,我不明白你的意思,因为你有自己的 utf8 strncpy,它可以很容易地调整为你喜欢的关于 NULL 终止的行为。
  • 实现这一点的iconv界面非常简单:只需将utf8转换为utf8并故意缩短outbytesleft。

标签: c++ c utf-8 strncpy


【解决方案1】:

我已经在许多带有多字节字符的示例 UTF8 字符串上对此进行了测试。如果源太长,它会对其进行反向搜索(从空终止符开始)并向后工作以找到最后一个可以放入目标缓冲区的完整 UTF8 字符。它始终确保目的地为空终止。

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;
}

【讨论】:

  • 这是(几乎)这里最好的算法。我很震惊(震惊!)没有其他人利用 UTF-8 的自同步能力和 UTF 字符的 4 字节最大长度作为简单查找最后一个完整字符的基础通过从末尾和memcpy 到最后一个完整的UTF-8 字符的所有内容进行有限搜索。不过我建议如果确实是sizeSrc >= sizeDst,那么从lastByte = src + sizeDst 开始;它会更快,并且最多需要循环 4 次迭代。
【解决方案2】:

我不确定你所说的 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;
}

(utf8Size中表格的内容生成起来有点痛苦, 但这是一个你会经常使用的功能,如果你正在处理 UTF-8,你只需要做一次。)

【讨论】:

  • 顺便说一句,你的函数不像strncpy,更像strlcpy
  • @Hans,为什么是 64MB?您只需检查first byte 即可获取当前长度。
  • 其实@Hans,一个无符号字符只有256个可能的值。
  • 啊,对。不是很了解代码,不知道 ch 的价值在哪里。
  • Unicode 字符的 UTF-8 表示的长度不能超过 4 个字节。较早的提案指定了 5 字节和 6 字节序列,但现代 UTF-8 最高为 4 字节。
【解决方案3】:

strncpy() 是个糟糕的函数:

  1. 如果空间不足,生成的字符串将不会以 nul 结尾
  2. 如果有足够的空间,剩余的空间将被 NUL 填充。如果目标字符串非常大,这可能会很痛苦。

即使字符保持在 ASCII 范围内(0x7f 及以下),生成的字符串也不会是您想要的。在 UTF-8 情况下,它可能不是以 nul 结尾的并且以无效的 UTF-8 序列结尾。

最好的建议是避免strncpy()

编辑: 广告 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;
}

【讨论】:

  • 请注意,if 结果是正确的 C 字符串(即以 nul 结尾),那么它也是正确的 UTF-8 字符串(即没有部分字符)。如果它不是一个正确的 C 字符串,那么无论如何你都应该使用错误处理程序。 strncpy 只是确保您可以安全地访问该错误处理程序。
  • UTF-8 向后兼容 ASCII;即所有 ASCII 字符串都是有效的 UTF-8 字符串。
  • 但截断的 utf8 字符串无效。在这两种情况下,未终止的字符串都是错误的。 strncpy 比它试图解决的问题更糟糕。顺便说一句:即使您“解决”了问题并生成了一个有效且终止的 utf8(或纯 ascii)字符串,它仍然会被截断。字符串的前 xxx 个字符的语义值是什么?程序不会崩溃,但你真的想要它的结果吗?
  • 我认为你的回答没有抓住重点——关于 strcpy / strncpy / strcpy_s 的讨论很多——以及如何最好地处理 NULL 终止。我的问题是,似乎没有任何常用的函数可以复制 utf8、限制缓冲区大小并确保生成的字符串也是有效的 utf8。
  • @MSalters:如果您没有正确的 C 字符串,则不要调用需要正确 C 字符串的函数。您不会使用 http URL 调用 fopen,那么为什么要使用不是字符串的东西调用 str* 函数呢?
【解决方案4】:

为了回答自己的问题,这是我最终得到的 C 函数(此项目不使用 C++):

注意事项: - 意识到这不是 utf8 的 strncpy 的克隆,它更像是来自 openbsd 的 strlcpy。 - 从 glib 的 gutf8.c 复制的 utf8_skip_data - 它不验证 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;
}

【讨论】:

  • maxncpy 为 0 时有一个小问题。在这种情况下,dst 仍然被取消引用并分配了 '\0'。此外,它的性能可能不是很好。如果你能保证dstsrc不重叠,那么你可以使用C99restrict keyword。否则,您可以计算要复制的字节数,然后调用memmove
  • @Daniel Trebbien,感谢您的提示,在我为项目提供的版本中,restrict 现在已使用。 amd 更新了检查 maxncpy>0 的函数
  • 您正在假设无符号字符。许多实现默认使用签名字符。
  • @wildplasser,好点,我用 gcc 的 -funsigned-char 构建,将更新示例。
  • 第二点:如果 strlen(src) 恰好 >= maxncpy,您的 *dst= '\0'; 将写入缓冲区之外。 (strncpy() 行为将使 dst 字符串未终止...)
【解决方案5】:

这是一个 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

我认为这几乎就是您要查找的内容,但请注意,如果组合字符适用于 nth 字符(本身不是组合字符),并且目标缓冲区刚好足够存储字符 1 到 n 的 UTF-8 编码,但不能存储字符的组合字符n。在这种情况下,表示字符 1 到 n 的字节被写入,但 n 的组合字符都没有写入。实际上,您可以说 nth 字符是部分写入的。

【讨论】:

    【解决方案6】:

    评论上面的答案“strncpy() 是一个糟糕的函数:”。 我什至讨厌以创建另一个互联网编程圣战为代价来评论这种笼统的陈述,但无论如何,这样的陈述会误导那些可能来这里寻找答案的人。

    好吧,也许 C 字符串函数是“老派”。也许 C/C++ 中的所有字符串都应该在某种智能容器中,等等,也许应该使用 C++ 而不是 C(当你有选择的时候),这些更多是其他主题的偏好和论据。

    我来这里是为了寻找我自己的 UTF-8 strncpy()。并不是说我不能制作一个(恕我直言,编码简单而优雅),而是想看看其他人是如何制作的,也许可以在 ASM 中找到一个优化的。

    对于编程界人的“上帝的礼物”,暂时放下你的傲慢,看看一些事实。

    “strncpy()”或任何其他具有相同副作用和“_snprintf()”等问题的类似函数没有任何问题。

    我说:“strncpy() 并不糟糕”,而是“糟糕的程序员用得非常糟糕”。

    什么是“可怕的”不知道规则。 此外,由于安全性(如缓冲区溢出)和程序稳定性影响,整个主题中,如果只是遵循规则,则不需要例如 Microsoft 将“安全字符串函数”添加到它的 CRT 库中。

    主要的:

    1. “sizeof()”返回带终止符的静态字符串的长度。
    2. "strlen()" 返回不带终止符的字符串长度。
    3. 大多数(如果不是)所有“n”函数只是钳制到“n”而不添加终止符。
    4. 在需要和输入缓冲区大小的函数中,“缓冲区大小”的含义存在隐含的歧义。 IE。 “(char *pszBuffer, int iBufferSize)”类型。 更安全地假设最坏的情况并传递比实际缓冲区大小小一的大小,并在末尾添加一个终止符以确保。
    5. 对于字符串输入、缓冲区等,根据预期的平均值和最大值设置和使用合理的大小限制。希望避免输入截断,并消除缓冲区溢出期。

    这就是我个人处理此类事情的方式,以及其他需要了解和实践的规则。

    一个方便的静态字符串大小宏:

    // 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()”。

    【讨论】:

    • “希望避免输入截断” 希望编程不是我认为的“安全实践”。我更喜欢在字符串被截断时总是得到一个信号,因为大多数时候截断应该被视为错误,而不是默默地忽略它。一个不给我这样的信号的函数是不可能的,不管它本身是否很糟糕,它的功能是否有一个糟糕的名字,或者对于预期目的来说是一个糟糕的选择。
    • 请注意,您添加了大量脆弱的逻辑,只是为了弥补 strncpy() 的缺点。 IMO 创建自己的函数会更容易(它会完全做你想要的)而不是尝试让 strncpy() 跳过箍。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-03-11
    • 2015-02-18
    • 1970-01-01
    • 2015-08-14
    • 1970-01-01
    • 2013-10-20
    • 2012-08-29
    相关资源
    最近更新 更多