| /****************************************************************************** | |
| * Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | |
| * | |
| * Test the xutf UTF-8 handler (src/common/xutf.c) directly. | |
| * | |
| * The cases pin the handler against The Unicode Standard, Version 15.0: | |
| * - Table 3-7 "Well-Formed UTF-8 Byte Sequences" (the canonical accept/reject | |
| * specification for UTF-8 byte sequences), | |
| * - D14 noncharacters, D49 private-use, D71-D73 surrogates, D92 UTF-8 (the | |
| * shortest-form / no-overlong requirement), | |
| * - the U+0000..U+10FFFF codespace bounds, | |
| * - the BOM signatures of Table 2-4, | |
| * - the newline (5.8), space (Table 6-2) and C0/C1 control sets. | |
| *****************************************************************************/ | |
| #include <check.h> | |
| #include <inttypes.h> | |
| #include <stdint.h> | |
| #include <stdlib.h> | |
| #include <string.h> | |
| #include "slurm/slurm_errno.h" | |
| #include "src/common/log.h" | |
| #include "src/common/read_config.h" | |
| #include "src/common/xmalloc.h" | |
| #include "src/common/xstring.h" | |
| #include "src/common/xutf.h" | |
| /* | |
| * Build a const utf8_t[] compound literal inline so a byte sequence can be | |
| * written as RB(0xED, 0xA0, 0x80). The literal has automatic storage tied to | |
| * the enclosing block, so it stays valid for the helper call it is passed to. | |
| */ | |
| #define RB(...) ((const utf8_t[]) { __VA_ARGS__ }) | |
| /* ------------------------------------------------------------------ helpers */ | |
| /* assert that src[0..len) decodes to exactly exp_code in exp_bytes bytes */ | |
| static void assert_read(const char *desc, const utf8_t *src, size_t len, | |
| bool check_valid, utf_code_t exp_code, int exp_bytes) | |
| { | |
| utf_code_t utf = 0; | |
| int bytes = 0; | |
| int rc = utf8_read_character(src, src + len, &utf, &bytes, check_valid); | |
| ck_assert_msg(rc == SLURM_SUCCESS, | |
| "%s: expected success, got rc=%d (%s)", desc, rc, | |
| slurm_strerror(rc)); | |
| ck_assert_msg(utf == exp_code, | |
| "%s: expected U+%06" PRIX32 ", got U+%06" PRIX32, desc, | |
| exp_code, utf); | |
| ck_assert_msg(bytes == exp_bytes, "%s: expected %d bytes, got %d", desc, | |
| exp_bytes, bytes); | |
| } | |
| /* | |
| * assert that src[0..len) is rejected. If exp_rc is non-zero, the exact error | |
| * code must match. On rejection the handler sets *utf to UTF_REPLACEMENT_CODE | |
| * and *bytes to the resync count, which must equal exp_bytes exactly. | |
| * | |
| * exp_bytes is how far a scanning caller advances past an ill-formed sequence. | |
| * An off-by-one there silently swallows the character that follows it. | |
| */ | |
| static void assert_not_read(const char *desc, const utf8_t *src, size_t len, | |
| bool check_valid, int exp_rc, int exp_bytes) | |
| { | |
| utf_code_t utf = 0; | |
| int bytes = 0; | |
| int rc = utf8_read_character(src, src + len, &utf, &bytes, check_valid); | |
| ck_assert_msg(rc != SLURM_SUCCESS, | |
| "%s: expected rejection, got success (U+%06" PRIX32 ")", | |
| desc, utf); | |
| ck_assert_msg(rc == exp_rc, "%s: expected rc=%d (%s), got rc=%d (%s)", | |
| desc, exp_rc, slurm_strerror(exp_rc), rc, | |
| slurm_strerror(rc)); | |
| ck_assert_msg(utf == UTF_REPLACEMENT_CODE, | |
| "%s: utf not U+FFFD on error (got U+%06" PRIX32 ")", desc, | |
| utf); | |
| ck_assert_msg(bytes == exp_bytes, | |
| "%s: expected resync of %d bytes, got %d", desc, | |
| exp_bytes, bytes); | |
| /* a resync must always make progress and stay inside the buffer */ | |
| ck_assert_msg((bytes >= 1) && (bytes <= (int) len), | |
| "%s: resync count out of range (got %d, len %zu)", desc, | |
| bytes, len); | |
| } | |
| /* one code unit produced by the read-and-advance loop */ | |
| typedef struct { | |
| utf_code_t utf; /* decoded scalar, or UTF_REPLACEMENT_CODE */ | |
| int bytes; /* bytes consumed producing it */ | |
| } utf_step_t; | |
| /* | |
| * Feed src[0..len) through the read-and-advance loop that callers scanning a | |
| * whole string use, and assert the exact sequence of (code, advance) pairs it | |
| * produces against exp[0..exp_cnt). | |
| * | |
| * The advance decides how many U+FFFD a scanner emits and which byte it resumes | |
| * on. A loop that over-advances drops the valid character following an | |
| * ill-formed sequence without raising any error. | |
| * | |
| * NOTE: several of these expectations deliberately differ from Unicode 15.0 3.9 | |
| * "U+FFFD Substitution of Maximal Subparts". The reader accepts any 0x80..0xBF | |
| * continuation byte rather than applying Table 3-7's per-lead second-byte | |
| * ranges, so it rejects by code value afterwards and emits one U+FFFD for the | |
| * whole sequence where 3.9 calls for one per byte (E0 80 80, ED A0 80, | |
| * F0 80 80 80, F4 90 80 80), and conversely splits E0 A0 into two where 3.9 | |
| * gives one. These pin the handler as it behaves today; if the reader is ever | |
| * made Table 3-7 conformant, these are the expectations to update. | |
| */ | |
| static void assert_read_sequence(const char *desc, const utf8_t *src, | |
| size_t len, const utf_step_t *exp, | |
| size_t exp_cnt) | |
| { | |
| const utf8_t *ptr = src; | |
| const utf8_t *end = src + len; | |
| size_t i = 0; | |
| while (ptr < end) { | |
| utf_code_t utf = 0; | |
| int bytes = 0; | |
| int rc = utf8_read_character(ptr, end, &utf, &bytes, true); | |
| ck_assert_msg(i < exp_cnt, | |
| "%s: produced more than the %zu expected units", | |
| desc, exp_cnt); | |
| ck_assert_msg((bytes >= 1) && (bytes <= (end - ptr)), | |
| "%s: advance %d out of range (%ld remaining)", | |
| desc, bytes, (long) (end - ptr)); | |
| ck_assert_msg(utf == exp[i].utf, | |
| "%s: unit %zu expected U+%06" PRIX32 | |
| ", got U+%06" PRIX32, | |
| desc, i, exp[i].utf, utf); | |
| ck_assert_msg(bytes == exp[i].bytes, | |
| "%s: unit %zu expected advance %d, got %d", desc, | |
| i, exp[i].bytes, bytes); | |
| if (rc != SLURM_SUCCESS) | |
| ck_assert_msg(utf == UTF_REPLACEMENT_CODE, | |
| "%s: gave U+%06" PRIX32 ", not U+FFFD", | |
| desc, utf); | |
| ptr += bytes; | |
| i++; | |
| } | |
| ck_assert_msg(i == exp_cnt, "%s: expected %zu units, got %zu", desc, | |
| exp_cnt, i); | |
| ck_assert_msg(ptr == end, "%s: buffer not fully consumed", desc); | |
| } | |
| /* call assert_read_sequence() with an inline expectation array */ | |
| #define ASSERT_SEQ(desc, src, len, ...) \ | |
| do { \ | |
| const utf_step_t _exp[] = { __VA_ARGS__ }; \ | |
| assert_read_sequence((desc), (src), (len), _exp, \ | |
| ARRAY_SIZE(_exp)); \ | |
| } while (false) | |
| /* assert that utf encodes to exactly the exp[0..exp_len) byte sequence */ | |
| static void assert_write(const char *desc, utf_code_t utf, const utf8_t *exp, | |
| int exp_len) | |
| { | |
| utf8_t dst[UTF8_CHAR_MAX_BYTES] = { 0 }; | |
| int bytes = 0; | |
| /* | |
| * The public macro is what callers use; it handles ASCII inline without | |
| * entering the function. test_write_ascii_direct() covers the | |
| * function's own ASCII branch. | |
| */ | |
| int rc = utf8_write_character(utf, dst, &bytes); | |
| ck_assert_msg(rc == SLURM_SUCCESS, "%s: write rc=%d (%s)", desc, rc, | |
| slurm_strerror(rc)); | |
| ck_assert_msg(bytes == exp_len, "%s: expected %d bytes, got %d", desc, | |
| exp_len, bytes); | |
| ck_assert_msg(!memcmp(dst, exp, exp_len), | |
| "%s: encoded bytes do not match expected", desc); | |
| } | |
| static void setup(void) | |
| { | |
| log_options_t log_opts = LOG_OPTS_INITIALIZER; | |
| const char *debug_env = getenv("SLURM_DEBUG"); | |
| const char *debug_flags_env = getenv("SLURM_DEBUG_FLAGS"); | |
| /* | |
| * xutf's LOG() is gated on DEBUG_FLAG_DATA plus debug5, and that trace | |
| * is the practical way to find which of the 1114111 code points | |
| * test_codespace_roundtrip failed on. | |
| */ | |
| if (debug_env) | |
| log_opts.stderr_level = log_string2num(debug_env); | |
| if (debug_flags_env) | |
| debug_str2flags(debug_flags_env, &slurm_conf.debug_flags); | |
| log_init("xutf-test", log_opts, 0, NULL); | |
| } | |
| static void teardown(void) | |
| { | |
| log_fini(); | |
| } | |
| /* ------------------------------ slurm_read_utf8_character: Table 3-7 */ | |
| /* Table 3-7 row 1: U+0000..U+007F (single ASCII byte) */ | |
| START_TEST(test_read_ascii) | |
| { | |
| assert_read("U+0001", RB(0x01), 1, true, 0x01, 1); | |
| assert_read("U+0041 'A'", RB(0x41), 1, true, 0x41, 1); | |
| assert_read("U+007F boundary", RB(0x7F), 1, true, 0x7F, 1); | |
| /* U+0000 is rejected by policy (check_valid only) */ | |
| assert_not_read("U+0000 NUL", RB(0x00), 1, true, ESLURM_UTF_NULL_CODE, | |
| 1); | |
| } | |
| END_TEST | |
| /* Table 3-7 row 2: U+0080..U+07FF, lead C2..DF */ | |
| START_TEST(test_read_two_byte) | |
| { | |
| assert_read("U+0080 low boundary", RB(0xC2, 0x80), 2, true, 0x0080, 2); | |
| assert_read("U+00F1 'n-tilde'", RB(0xC3, 0xB1), 2, true, 0x00F1, 2); | |
| assert_read("U+07FF high boundary", RB(0xDF, 0xBF), 2, true, 0x07FF, 2); | |
| } | |
| END_TEST | |
| /* Table 3-7 rows 3-6: U+0800..U+FFFF, leads E0,E1..EC,ED,EE..EF */ | |
| START_TEST(test_read_three_byte) | |
| { | |
| assert_read("U+0800 row-3 low", RB(0xE0, 0xA0, 0x80), 3, true, 0x0800, | |
| 3); | |
| assert_read("U+0FFF row-3 high", RB(0xE0, 0xBF, 0xBF), 3, true, 0x0FFF, | |
| 3); | |
| assert_read("U+1000 row-4 low", RB(0xE1, 0x80, 0x80), 3, true, 0x1000, | |
| 3); | |
| assert_read("U+20AC euro", RB(0xE2, 0x82, 0xAC), 3, true, 0x20AC, 3); | |
| assert_read("U+CFFF row-4 high", RB(0xEC, 0xBF, 0xBF), 3, true, 0xCFFF, | |
| 3); | |
| assert_read("U+D000 row-5 low", RB(0xED, 0x80, 0x80), 3, true, 0xD000, | |
| 3); | |
| assert_read("U+D7FF row-5 high", RB(0xED, 0x9F, 0xBF), 3, true, 0xD7FF, | |
| 3); | |
| assert_read("U+FFFD replacement", RB(0xEF, 0xBF, 0xBD), 3, true, 0xFFFD, | |
| 3); | |
| } | |
| END_TEST | |
| /* Table 3-7 rows 7-9: U+10000..U+10FFFF, leads F0,F1..F3,F4 */ | |
| START_TEST(test_read_four_byte) | |
| { | |
| assert_read("U+10000 row-7 low", RB(0xF0, 0x90, 0x80, 0x80), 4, true, | |
| 0x10000, 4); | |
| assert_read("U+1F600 emoji", RB(0xF0, 0x9F, 0x98, 0x80), 4, true, | |
| 0x1F600, 4); | |
| assert_read("U+40000 row-8 low", RB(0xF1, 0x80, 0x80, 0x80), 4, true, | |
| 0x40000, 4); | |
| assert_read("U+80000 row-8 mid", RB(0xF2, 0x80, 0x80, 0x80), 4, true, | |
| 0x80000, 4); | |
| assert_read("U+E0100 plane-14", RB(0xF3, 0xA0, 0x84, 0x80), 4, true, | |
| 0xE0100, 4); | |
| /* U+100000..U+10FFFF (row 9, lead F4) is all private-use or | |
| * noncharacters, so there is no valid plane-16 scalar to accept here; | |
| * its boundaries are exercised by the private/noncharacter cases. */ | |
| } | |
| END_TEST | |
| /* | |
| * D92 / Table 3-7: a code point encoded in more bytes than the minimum is | |
| * ill-formed (overlong). The C0 and C1 lead bytes can only ever introduce an | |
| * overlong two-byte form, so the handler rejects them through the overlong | |
| * check (ESLURM_UTF8_INVALID_READ), not as structurally illegal leads. | |
| */ | |
| START_TEST(test_read_overlong) | |
| { | |
| assert_not_read("overlong U+0000 (C0 80)", RB(0xC0, 0x80), 2, true, | |
| ESLURM_UTF8_INVALID_READ, 2); | |
| assert_not_read("overlong U+007F (C1 BF)", RB(0xC1, 0xBF), 2, true, | |
| ESLURM_UTF8_INVALID_READ, 2); | |
| assert_not_read("overlong U+0000 (E0 80 80)", RB(0xE0, 0x80, 0x80), 3, | |
| true, ESLURM_UTF8_INVALID_READ, 3); | |
| assert_not_read("overlong U+07FF (E0 9F BF)", RB(0xE0, 0x9F, 0xBF), 3, | |
| true, ESLURM_UTF8_INVALID_READ, 3); | |
| assert_not_read("overlong U+0000 (F0 80 80 80)", | |
| RB(0xF0, 0x80, 0x80, 0x80), 4, true, | |
| ESLURM_UTF8_INVALID_READ, 4); | |
| assert_not_read("overlong U+FFFF (F0 8F BF BF)", | |
| RB(0xF0, 0x8F, 0xBF, 0xBF), 4, true, | |
| ESLURM_UTF8_INVALID_READ, 4); | |
| } | |
| END_TEST | |
| /* | |
| * D71-D73: surrogate code points U+D800..U+DFFF are ill-formed in UTF-8 and are | |
| * rejected by utf_is_valid() when check_valid is set. | |
| */ | |
| START_TEST(test_read_surrogate_bytes) | |
| { | |
| assert_not_read("U+D800 surrogate", RB(0xED, 0xA0, 0x80), 3, true, | |
| ESLURM_UTF16_SURROGATE_CODE, 3); | |
| assert_not_read("U+DFFF surrogate", RB(0xED, 0xBF, 0xBF), 3, true, | |
| ESLURM_UTF16_SURROGATE_CODE, 3); | |
| } | |
| END_TEST | |
| /* Codespace bound: U+10FFFF is the maximum; anything above is ill-formed. */ | |
| START_TEST(test_read_above_max) | |
| { | |
| assert_not_read("U+110000 (F4 90 80 80)", RB(0xF4, 0x90, 0x80, 0x80), 4, | |
| true, ESLURM_UTF_INVALID_CODE, 4); | |
| } | |
| END_TEST | |
| /* | |
| * Table 3-7 lists no row whose lead byte is F5..FF: those bytes can never begin | |
| * a sequence. This handler splits that set. F8..FF are refused structurally as | |
| * bad leads (test_read_illegal_lead), but the 4-byte branch masks with | |
| * (*src & 0xf8) == 0xf0, which matches F0..F7 -- so F5, F6 and F7 decode as | |
| * 4-byte leads, yielding U+140000..U+1FFFFF, and are caught only afterwards by | |
| * the codespace bound inside utf_is_valid(). | |
| * | |
| * That makes F5..F7 the only Table 3-7 structural violation gated behind | |
| * check_valid; overlongs, bad continuations, truncation and F8..FF are all | |
| * unconditional. With check_valid=false these decode successfully to | |
| * out-of-codespace scalars (test_read_no_check_valid). | |
| * | |
| * If the reader is made Table 3-7 conformant, the expectation becomes | |
| * ESLURM_UTF8_INVALID_READ with a 1-byte resync. | |
| */ | |
| START_TEST(test_read_out_of_range_lead) | |
| { | |
| assert_not_read("U+140000 (F5 80 80 80)", RB(0xF5, 0x80, 0x80, 0x80), 4, | |
| true, ESLURM_UTF_INVALID_CODE, 4); | |
| assert_not_read("U+180000 (F6 80 80 80)", RB(0xF6, 0x80, 0x80, 0x80), 4, | |
| true, ESLURM_UTF_INVALID_CODE, 4); | |
| assert_not_read("U+1FFFFF (F7 BF BF BF)", RB(0xF7, 0xBF, 0xBF, 0xBF), 4, | |
| true, ESLURM_UTF_INVALID_CODE, 4); | |
| /* a lone F5..F7 has no room for the 4-byte form it claims */ | |
| assert_not_read("truncated lead (F5)", RB(0xF5), 1, true, | |
| ESLURM_UTF8_READ_ILLEGAL_TERMINATION, 1); | |
| assert_not_read("truncated lead (F7)", RB(0xF7), 1, true, | |
| ESLURM_UTF8_READ_ILLEGAL_TERMINATION, 1); | |
| } | |
| END_TEST | |
| /* Table 3-7: bytes that are never valid leads, and stray continuation bytes. */ | |
| START_TEST(test_read_illegal_lead) | |
| { | |
| /* every continuation byte 0x80..0xBF is ill-formed as a lead */ | |
| for (int b = 0x80; b <= 0xBF; b++) { | |
| utf8_t seq[1] = { b }; | |
| char desc[32]; | |
| snprintf(desc, sizeof(desc), "stray continuation 0x%02X", b); | |
| assert_not_read(desc, seq, 1, true, ESLURM_UTF8_INVALID_READ, | |
| 1); | |
| } | |
| /* | |
| * F8..FF can never begin a UTF-8 sequence. F5..F7 are equally illegal | |
| * leads per Table 3-7 but are not refused here; see | |
| * test_read_out_of_range_lead(). | |
| */ | |
| for (int b = 0xF8; b <= 0xFF; b++) { | |
| utf8_t seq[1] = { b }; | |
| char desc[32]; | |
| snprintf(desc, sizeof(desc), "illegal lead 0x%02X", b); | |
| assert_not_read(desc, seq, 1, true, ESLURM_UTF8_INVALID_READ, | |
| 1); | |
| } | |
| } | |
| END_TEST | |
| /* | |
| * Table 3-7 / D92: a multibyte sequence that runs past the end of the buffer is | |
| * an illegal termination; a non-continuation in byte 2/3/4 is an invalid byte. | |
| */ | |
| START_TEST(test_read_truncated) | |
| { | |
| assert_not_read("2-byte truncated (C3)", RB(0xC3), 1, true, | |
| ESLURM_UTF8_READ_ILLEGAL_TERMINATION, 1); | |
| assert_not_read("3-byte truncated (E2 82)", RB(0xE2, 0x82), 2, true, | |
| ESLURM_UTF8_READ_ILLEGAL_TERMINATION, 1); | |
| assert_not_read("4-byte truncated (F0 9F 98)", RB(0xF0, 0x9F, 0x98), 3, | |
| true, ESLURM_UTF8_READ_ILLEGAL_TERMINATION, 1); | |
| assert_not_read("bad byte 2 (C2 41)", RB(0xC2, 0x41), 2, true, | |
| ESLURM_UTF8_INVALID_BYTE_2, 1); | |
| assert_not_read("bad byte 2 (E2 41 82)", RB(0xE2, 0x41, 0x82), 3, true, | |
| ESLURM_UTF8_INVALID_BYTE_2, 1); | |
| assert_not_read("bad byte 2 (F0 41 80 80)", RB(0xF0, 0x41, 0x80, 0x80), | |
| 4, true, ESLURM_UTF8_INVALID_BYTE_2, 1); | |
| assert_not_read("bad byte 3 (E2 82 41)", RB(0xE2, 0x82, 0x41), 3, true, | |
| ESLURM_UTF8_INVALID_BYTE_3, 2); | |
| assert_not_read("bad byte 3 (F0 9F 41 80)", RB(0xF0, 0x9F, 0x41, 0x80), | |
| 4, true, ESLURM_UTF8_INVALID_BYTE_3, 2); | |
| assert_not_read("bad byte 4 (F0 9F 98 41)", RB(0xF0, 0x9F, 0x98, 0x41), | |
| 4, true, ESLURM_UTF8_INVALID_BYTE_4, 3); | |
| } | |
| END_TEST | |
| /* | |
| * Ill-formed sequences must resync safely: every code unit produced and every | |
| * byte advance is pinned exactly. See assert_read_sequence() for where these | |
| * expectations knowingly differ from Unicode 15.0 3.9. | |
| */ | |
| START_TEST(test_read_maximal_subpart) | |
| { | |
| /* out-of-range 2nd byte / overlong / surrogate / above-max / bad lead */ | |
| ASSERT_SEQ("overlong E0 80 80", RB(0xE0, 0x80, 0x80), 3, | |
| { UTF_REPLACEMENT_CODE, 3 }); | |
| ASSERT_SEQ("overlong E0 9F BF", RB(0xE0, 0x9F, 0xBF), 3, | |
| { UTF_REPLACEMENT_CODE, 3 }); | |
| ASSERT_SEQ("surrogate ED A0 80", RB(0xED, 0xA0, 0x80), 3, | |
| { UTF_REPLACEMENT_CODE, 3 }); | |
| ASSERT_SEQ("overlong F0 80 80 80", RB(0xF0, 0x80, 0x80, 0x80), 4, | |
| { UTF_REPLACEMENT_CODE, 4 }); | |
| ASSERT_SEQ("above max F4 90 80 80", RB(0xF4, 0x90, 0x80, 0x80), 4, | |
| { UTF_REPLACEMENT_CODE, 4 }); | |
| ASSERT_SEQ("overlong C0 AF", RB(0xC0, 0xAF), 2, | |
| { UTF_REPLACEMENT_CODE, 2 }); | |
| ASSERT_SEQ("bad lead F5 80 80 80", RB(0xF5, 0x80, 0x80, 0x80), 4, | |
| { UTF_REPLACEMENT_CODE, 4 }); | |
| ASSERT_SEQ("lone continuation 80", RB(0x80), 1, | |
| { UTF_REPLACEMENT_CODE, 1 }); | |
| /* | |
| * Bad continuation byte followed by decodable ASCII. Each sequence is | |
| * long enough to reach the continuation-byte check it is named for; a | |
| * shorter buffer trips the length guard first and lands in the | |
| * truncation cases below instead. | |
| */ | |
| ASSERT_SEQ("bad 2nd C2 41", RB(0xC2, 0x41), 2, | |
| { UTF_REPLACEMENT_CODE, 1 }, { 0x41, 1 }); | |
| ASSERT_SEQ("bad 2nd E0 41 42", RB(0xE0, 0x41, 0x42), 3, | |
| { UTF_REPLACEMENT_CODE, 1 }, { 0x41, 1 }, { 0x42, 1 }); | |
| ASSERT_SEQ("bad 2nd F0 41 42 43", RB(0xF0, 0x41, 0x42, 0x43), 4, | |
| { UTF_REPLACEMENT_CODE, 1 }, { 0x41, 1 }, { 0x42, 1 }, | |
| { 0x43, 1 }); | |
| ASSERT_SEQ("bad 3rd E2 82 41", RB(0xE2, 0x82, 0x41), 3, | |
| { UTF_REPLACEMENT_CODE, 2 }, { 0x41, 1 }); | |
| ASSERT_SEQ("bad 3rd F0 90 41 42", RB(0xF0, 0x90, 0x41, 0x42), 4, | |
| { UTF_REPLACEMENT_CODE, 2 }, { 0x41, 1 }, { 0x42, 1 }); | |
| ASSERT_SEQ("bad 4th F0 90 80 41", RB(0xF0, 0x90, 0x80, 0x41), 4, | |
| { UTF_REPLACEMENT_CODE, 3 }, { 0x41, 1 }); | |
| /* sequences truncated at the end of the buffer */ | |
| ASSERT_SEQ("truncated E0", RB(0xE0), 1, { UTF_REPLACEMENT_CODE, 1 }); | |
| ASSERT_SEQ("truncated E0 A0", RB(0xE0, 0xA0), 2, | |
| { UTF_REPLACEMENT_CODE, 1 }, { UTF_REPLACEMENT_CODE, 1 }); | |
| ASSERT_SEQ("truncated F0 90", RB(0xF0, 0x90), 2, | |
| { UTF_REPLACEMENT_CODE, 1 }, { UTF_REPLACEMENT_CODE, 1 }); | |
| ASSERT_SEQ("truncated F0 90 80", RB(0xF0, 0x90, 0x80), 3, | |
| { UTF_REPLACEMENT_CODE, 1 }, { UTF_REPLACEMENT_CODE, 1 }, | |
| { UTF_REPLACEMENT_CODE, 1 }); | |
| /* mixed valid and ill-formed, exercising resync across boundaries */ | |
| ASSERT_SEQ("mixed valid/ill-formed", | |
| RB(0x61, 0xF1, 0x80, 0x80, 0xE1, 0x80, 0xC2, 0x62, 0x80, | |
| 0x63, 0x80, 0xBF, 0x64), | |
| 13, { 0x61, 1 }, { UTF_REPLACEMENT_CODE, 3 }, | |
| { UTF_REPLACEMENT_CODE, 2 }, { UTF_REPLACEMENT_CODE, 1 }, | |
| { 0x62, 1 }, { UTF_REPLACEMENT_CODE, 1 }, { 0x63, 1 }, | |
| { UTF_REPLACEMENT_CODE, 1 }, { UTF_REPLACEMENT_CODE, 1 }, | |
| { 0x64, 1 }); | |
| } | |
| END_TEST | |
| /* | |
| * With check_valid=true the decoder also applies utf_is_valid() policy: it | |
| * rejects noncharacters, private-use and reserved scalars even though their | |
| * byte sequences are structurally well-formed. | |
| */ | |
| START_TEST(test_read_policy_rejects) | |
| { | |
| assert_not_read("U+E000 private (EE 80 80)", RB(0xEE, 0x80, 0x80), 3, | |
| true, ESLURM_UTF_PRIVATE_CODE, 3); | |
| assert_not_read("U+FFFE noncharacter (EF BF BE)", RB(0xEF, 0xBF, 0xBE), | |
| 3, true, ESLURM_UTF_NONCHARACTER_CODE, 3); | |
| assert_not_read("U+FFFF noncharacter (EF BF BF)", RB(0xEF, 0xBF, 0xBF), | |
| 3, true, ESLURM_UTF_NONCHARACTER_CODE, 3); | |
| assert_not_read("U+FFF0 reserved (EF BF B0)", RB(0xEF, 0xBF, 0xB0), 3, | |
| true, ESLURM_UTF_RESERVED_CODE, 3); | |
| assert_not_read("U+10FFFF noncharacter (F4 8F BF BF)", | |
| RB(0xF4, 0x8F, 0xBF, 0xBF), 4, true, | |
| ESLURM_UTF_NONCHARACTER_CODE, 4); | |
| assert_not_read("U+100000 private (F4 80 80 80)", | |
| RB(0xF4, 0x80, 0x80, 0x80), 4, true, | |
| ESLURM_UTF_PRIVATE_CODE, 4); | |
| } | |
| END_TEST | |
| /* | |
| * check_valid=false skips the utf_is_valid() policy and nothing else, and | |
| * nothing asserts on that path, so the mode returns whatever the byte pattern | |
| * decodes to -- including scalars the standard forbids. | |
| * | |
| * The reader has no structural surrogate or above-max check at all; it rejects | |
| * those solely via the policy call. So the mode is only observable in what it | |
| * ACCEPTS, which is what the second block below pins. | |
| */ | |
| START_TEST(test_read_no_check_valid) | |
| { | |
| assert_read("U+0041 (no check)", RB(0x41), 1, false, 0x41, 1); | |
| assert_read("U+00F1 (no check)", RB(0xC3, 0xB1), 2, false, 0x00F1, 2); | |
| assert_read("U+20AC (no check)", RB(0xE2, 0x82, 0xAC), 3, false, 0x20AC, | |
| 3); | |
| assert_read("U+1F600 (no check)", RB(0xF0, 0x9F, 0x98, 0x80), 4, false, | |
| 0x1F600, 4); | |
| /* | |
| * Policy-invalid but structurally parsable input passes straight | |
| * through. Each of these is rejected when check_valid is set. | |
| */ | |
| assert_read("U+0000 NUL (no check)", RB(0x00), 1, false, 0x0, 1); | |
| assert_read("U+D800 surrogate (no check)", RB(0xED, 0xA0, 0x80), 3, | |
| false, 0xD800, 3); | |
| assert_read("U+E000 private (no check)", RB(0xEE, 0x80, 0x80), 3, false, | |
| 0xE000, 3); | |
| assert_read("U+FFFF noncharacter (no check)", RB(0xEF, 0xBF, 0xBF), 3, | |
| false, 0xFFFF, 3); | |
| assert_read("U+110000 above max (no check)", RB(0xF4, 0x90, 0x80, 0x80), | |
| 4, false, 0x110000, 4); | |
| assert_read("U+1FFFFF lead F7 (no check)", RB(0xF7, 0xBF, 0xBF, 0xBF), | |
| 4, false, 0x1FFFFF, 4); | |
| /* structural rejects are unconditional and still fire */ | |
| assert_not_read("overlong C0 80 (no check)", RB(0xC0, 0x80), 2, false, | |
| ESLURM_UTF8_INVALID_READ, 2); | |
| assert_not_read("stray continuation 80 (no check)", RB(0x80), 1, false, | |
| ESLURM_UTF8_INVALID_READ, 1); | |
| assert_not_read("illegal lead F8 (no check)", RB(0xF8), 1, false, | |
| ESLURM_UTF8_INVALID_READ, 1); | |
| assert_not_read("truncated C3 (no check)", RB(0xC3), 1, false, | |
| ESLURM_UTF8_READ_ILLEGAL_TERMINATION, 1); | |
| assert_not_read("bad byte 2 (C2 41) (no check)", RB(0xC2, 0x41), 2, | |
| false, ESLURM_UTF8_INVALID_BYTE_2, 1); | |
| } | |
| END_TEST | |
| /* | |
| * An empty range satisfies every xassert (src <= end, remaining >= 0) and is | |
| * the state a scanning loop reaches at string termination. It is also the only | |
| * path returning bytes = 0, so a caller advancing on bytes without checking rc | |
| * would spin. | |
| */ | |
| START_TEST(test_read_empty_range) | |
| { | |
| const utf8_t *src = RB(0x41); | |
| utf_code_t utf = 0xDEAD; | |
| int bytes = -1; | |
| ck_assert_int_eq(utf8_read_character(src, src, &utf, &bytes, true), | |
| EINVAL); | |
| ck_assert_int_eq(bytes, 0); | |
| ck_assert_int_eq(utf, 0); | |
| } | |
| END_TEST | |
| /* The utf8_read_character() macro short-circuits the ASCII fast path. */ | |
| START_TEST(test_read_macro) | |
| { | |
| const utf8_t *src; | |
| const utf8_t *end; | |
| utf_code_t utf = 0; | |
| int bytes = 0; | |
| src = RB(0x41); | |
| end = src + 1; | |
| ck_assert_int_eq(utf8_read_character(src, end, &utf, &bytes, true), | |
| SLURM_SUCCESS); | |
| ck_assert_int_eq(utf, 0x41); | |
| ck_assert_int_eq(bytes, 1); | |
| /* non-ASCII delegates to slurm_read_utf8_character() */ | |
| src = RB(0xE2, 0x82, 0xAC); | |
| end = src + 3; | |
| utf = 0; | |
| bytes = 0; | |
| ck_assert_int_eq(utf8_read_character(src, end, &utf, &bytes, true), | |
| SLURM_SUCCESS); | |
| ck_assert_int_eq(utf, 0x20AC); | |
| ck_assert_int_eq(bytes, 3); | |
| } | |
| END_TEST | |
| /* ----------------------------------------------- slurm_write_utf8_character */ | |
| START_TEST(test_write_valid) | |
| { | |
| assert_write("U+0041 'A'", 0x41, RB(0x41), 1); | |
| assert_write("U+0080", 0x80, RB(0xC2, 0x80), 2); | |
| assert_write("U+07FF", 0x7FF, RB(0xDF, 0xBF), 2); | |
| assert_write("U+0800", 0x800, RB(0xE0, 0xA0, 0x80), 3); | |
| assert_write("U+20AC euro", 0x20AC, RB(0xE2, 0x82, 0xAC), 3); | |
| assert_write("U+FFFD replacement", 0xFFFD, RB(0xEF, 0xBF, 0xBD), 3); | |
| assert_write("U+10000", 0x10000, RB(0xF0, 0x90, 0x80, 0x80), 4); | |
| assert_write("U+1F600 emoji", 0x1F600, RB(0xF0, 0x9F, 0x98, 0x80), 4); | |
| assert_write("U+E0100 plane-14", 0xE0100, RB(0xF3, 0xA0, 0x84, 0x80), | |
| 4); | |
| } | |
| END_TEST | |
| /* invalid input is replaced with U+FFFD (3 bytes), never emitted raw */ | |
| START_TEST(test_write_invalid_to_replacement) | |
| { | |
| const utf8_t rep[] = { 0xEF, 0xBF, 0xBD }; /* U+FFFD */ | |
| assert_write("U+0000 -> replacement", 0x0, rep, 3); | |
| assert_write("U+D800 surrogate -> replacement", 0xD800, rep, 3); | |
| assert_write("U+FFFE noncharacter -> replacement", 0xFFFE, rep, 3); | |
| assert_write("U+E000 private -> replacement", 0xE000, rep, 3); | |
| assert_write("U+10FFFF noncharacter -> replacement", 0x10FFFF, rep, 3); | |
| assert_write("U+110000 above max -> replacement", 0x110000, rep, 3); | |
| /* | |
| * utf_code_t is unsigned, so -1 converts to 0xFFFFFFFF at the parameter | |
| * boundary and is rejected for being above U+10FFFF, not for being | |
| * negative. | |
| */ | |
| assert_write("0xFFFFFFFF -> replacement", -1, rep, 3); | |
| } | |
| END_TEST | |
| /* | |
| * The utf8_write_character() macro handles ASCII without entering the function, | |
| * so reach the function's own ASCII branch by calling it directly. Safe despite | |
| * slurm_is_utf_valid() asserting non-ASCII: the function tests validity via the | |
| * utf_is_valid() macro, which short-circuits ASCII and never calls it. | |
| */ | |
| START_TEST(test_write_ascii_direct) | |
| { | |
| utf8_t dst[UTF8_CHAR_MAX_BYTES] = { 0 }; | |
| int bytes = 0; | |
| ck_assert_int_eq(slurm_write_utf8_character(0x41, dst, &bytes, true), | |
| SLURM_SUCCESS); | |
| ck_assert_int_eq(bytes, 1); | |
| ck_assert_int_eq(dst[0], 0x41); | |
| ck_assert_int_eq(slurm_write_utf8_character(0x7F, dst, &bytes, true), | |
| SLURM_SUCCESS); | |
| ck_assert_int_eq(bytes, 1); | |
| ck_assert_int_eq(dst[0], 0x7F); | |
| /* log=false must behave identically */ | |
| ck_assert_int_eq(slurm_write_utf8_character(0x41, dst, &bytes, false), | |
| SLURM_SUCCESS); | |
| ck_assert_int_eq(bytes, 1); | |
| ck_assert_int_eq(dst[0], 0x41); | |
| } | |
| END_TEST | |
| /* ----------------------------------------------------- utf_is_valid() */ | |
| START_TEST(test_is_utf_valid) | |
| { | |
| /* valid scalars across the byte-length ranges */ | |
| ck_assert_int_eq(utf_is_valid(0x0001), SLURM_SUCCESS); | |
| ck_assert_int_eq(utf_is_valid(0x007F), SLURM_SUCCESS); | |
| ck_assert_int_eq(utf_is_valid(0x0080), SLURM_SUCCESS); | |
| ck_assert_int_eq(utf_is_valid(0x07FF), SLURM_SUCCESS); | |
| ck_assert_int_eq(utf_is_valid(0x0800), SLURM_SUCCESS); | |
| ck_assert_int_eq(utf_is_valid(0xD7FF), SLURM_SUCCESS); | |
| ck_assert_int_eq(utf_is_valid(0xFDCF), SLURM_SUCCESS); /* below FDD0 */ | |
| ck_assert_int_eq(utf_is_valid(0xFDF0), SLURM_SUCCESS); /* above FDEF */ | |
| ck_assert_int_eq(utf_is_valid(0xFEFF), | |
| SLURM_SUCCESS); /* BOM is valid */ | |
| ck_assert_int_eq(utf_is_valid(0xFFFD), SLURM_SUCCESS); | |
| ck_assert_int_eq(utf_is_valid(0x10000), SLURM_SUCCESS); | |
| ck_assert_int_eq(utf_is_valid(0x1FFFD), SLURM_SUCCESS); | |
| ck_assert_int_eq(utf_is_valid(0xE0100), SLURM_SUCCESS); | |
| /* U+0000 and out-of-range */ | |
| ck_assert_int_eq(utf_is_valid(0x0), ESLURM_UTF_NULL_CODE); | |
| /* unsigned utf_code_t: -1 converts to 0xFFFFFFFF, i.e. above U+10FFFF */ | |
| ck_assert_int_eq(utf_is_valid(-1), ESLURM_UTF_INVALID_CODE); | |
| ck_assert_int_eq(utf_is_valid(0x110000), ESLURM_UTF_INVALID_CODE); | |
| /* surrogates */ | |
| ck_assert_int_eq(utf_is_valid(0xD800), ESLURM_UTF16_SURROGATE_CODE); | |
| ck_assert_int_eq(utf_is_valid(0xDFFF), ESLURM_UTF16_SURROGATE_CODE); | |
| /* noncharacters: U+FDD0..U+FDEF and U+nFFFE/U+nFFFF for every plane */ | |
| ck_assert_int_eq(utf_is_valid(0xFDD0), ESLURM_UTF_NONCHARACTER_CODE); | |
| ck_assert_int_eq(utf_is_valid(0xFDEF), ESLURM_UTF_NONCHARACTER_CODE); | |
| ck_assert_int_eq(utf_is_valid(0xFFFE), ESLURM_UTF_NONCHARACTER_CODE); | |
| ck_assert_int_eq(utf_is_valid(0xFFFF), ESLURM_UTF_NONCHARACTER_CODE); | |
| ck_assert_int_eq(utf_is_valid(0x1FFFE), ESLURM_UTF_NONCHARACTER_CODE); | |
| ck_assert_int_eq(utf_is_valid(0x2FFFE), ESLURM_UTF_NONCHARACTER_CODE); | |
| ck_assert_int_eq(utf_is_valid(0xFFFFE), ESLURM_UTF_NONCHARACTER_CODE); | |
| ck_assert_int_eq(utf_is_valid(0x10FFFE), ESLURM_UTF_NONCHARACTER_CODE); | |
| ck_assert_int_eq(utf_is_valid(0x10FFFF), ESLURM_UTF_NONCHARACTER_CODE); | |
| /* private-use: all three D49 ranges */ | |
| ck_assert_int_eq(utf_is_valid(0xE000), ESLURM_UTF_PRIVATE_CODE); | |
| ck_assert_int_eq(utf_is_valid(0xF8FF), ESLURM_UTF_PRIVATE_CODE); | |
| ck_assert_int_eq(utf_is_valid(0xF0000), ESLURM_UTF_PRIVATE_CODE); | |
| ck_assert_int_eq(utf_is_valid(0xFFFFD), ESLURM_UTF_PRIVATE_CODE); | |
| ck_assert_int_eq(utf_is_valid(0x100000), ESLURM_UTF_PRIVATE_CODE); | |
| ck_assert_int_eq(utf_is_valid(0x10FFFD), ESLURM_UTF_PRIVATE_CODE); | |
| /* reserved U+FFF0..U+FFF8 */ | |
| ck_assert_int_eq(utf_is_valid(0xFFF0), ESLURM_UTF_RESERVED_CODE); | |
| ck_assert_int_eq(utf_is_valid(0xFFF8), ESLURM_UTF_RESERVED_CODE); | |
| } | |
| END_TEST | |
| /* ----------------------------------------------------- utf16_to_coding() */ | |
| START_TEST(test_utf16_to_coding) | |
| { | |
| utf_code_t utf = 0; | |
| /* BMP scalar (no low surrogate) */ | |
| ck_assert_int_eq(utf16_to_coding(0x0041, 0, &utf), SLURM_SUCCESS); | |
| ck_assert_int_eq(utf, 0x0041); | |
| ck_assert_int_eq(utf16_to_coding(0xD7FF, 0, &utf), SLURM_SUCCESS); | |
| ck_assert_int_eq(utf, 0xD7FF); | |
| /* valid surrogate pairs */ | |
| ck_assert_int_eq(utf16_to_coding(0xD834, 0xDD1E, &utf), SLURM_SUCCESS); | |
| ck_assert_int_eq(utf, 0x1D11E); /* G clef */ | |
| ck_assert_int_eq(utf16_to_coding(0xD800, 0xDC00, &utf), SLURM_SUCCESS); | |
| ck_assert_int_eq(utf, 0x10000); /* lowest supplementary */ | |
| /* lone / mismatched surrogates */ | |
| ck_assert_int_eq(utf16_to_coding(0xD800, 0, &utf), | |
| ESLURM_UTF16_SURROGATE_CODE); | |
| ck_assert_int_eq(utf16_to_coding(0xDC00, 0, &utf), | |
| ESLURM_UTF16_SURROGATE_CODE); | |
| ck_assert_int_eq(utf16_to_coding(0x0041, 0xDC00, &utf), | |
| ESLURM_UTF16_SURROGATE_CODE); | |
| ck_assert_int_eq(utf16_to_coding(0xD800, 0x0041, &utf), | |
| ESLURM_UTF16_SURROGATE_CODE); | |
| /* | |
| * Pairs whose low-surrogate offset has its top bit set (v % 0x400 >= | |
| * 0x200); the pairs above all have an offset below 0x200. | |
| */ | |
| ck_assert_int_eq(utf16_to_coding(0xD800, 0xDF00, &utf), SLURM_SUCCESS); | |
| ck_assert_int_eq(utf, 0x10300); | |
| ck_assert_int_eq(utf16_to_coding(0xD83D, 0xDE00, &utf), SLURM_SUCCESS); | |
| ck_assert_int_eq(utf, 0x1F600); | |
| /* a pair that resolves to a policy-rejected scalar */ | |
| ck_assert_int_eq(utf16_to_coding(0xDBFF, 0xDFFF, &utf), | |
| ESLURM_UTF_NONCHARACTER_CODE); /* U+10FFFF */ | |
| ck_assert_int_eq(utf16_to_coding(0xDB80, 0xDC00, &utf), | |
| ESLURM_UTF_PRIVATE_CODE); /* U+F0000 */ | |
| } | |
| END_TEST | |
| /* --------------------------------------------------- utf16_from_coding() */ | |
| START_TEST(test_utf16_from_coding) | |
| { | |
| utf16_t high = 0, low = 0; | |
| utf_code_t utf = 0; | |
| /* BMP scalar (no low surrogate) */ | |
| ck_assert_int_eq(utf16_from_coding(0x0041, &high, &low), SLURM_SUCCESS); | |
| ck_assert_int_eq(high, 0x0041); | |
| ck_assert_int_eq(low, 0); | |
| ck_assert_int_eq(utf16_from_coding(0xD7FF, &high, &low), SLURM_SUCCESS); | |
| ck_assert_int_eq(high, 0xD7FF); | |
| ck_assert_int_eq(low, 0); | |
| /* supplementary scalars split into surrogate pairs */ | |
| ck_assert_int_eq(utf16_from_coding(0x1D11E, &high, &low), | |
| SLURM_SUCCESS); | |
| ck_assert_int_eq(high, 0xD834); /* G clef */ | |
| ck_assert_int_eq(low, 0xDD1E); | |
| ck_assert_int_eq(utf16_from_coding(0x10000, &high, &low), | |
| SLURM_SUCCESS); | |
| ck_assert_int_eq(high, 0xD800); /* lowest supplementary */ | |
| ck_assert_int_eq(low, 0xDC00); | |
| /* | |
| * Scalars whose offset into the supplementary plane has bit 0x200 set; | |
| * U+1D11E (0x11E) and U+10000 (0x000) both leave it clear. | |
| */ | |
| ck_assert_int_eq(utf16_from_coding(0x10300, &high, &low), | |
| SLURM_SUCCESS); | |
| ck_assert_int_eq(high, 0xD800); | |
| ck_assert_int_eq(low, 0xDF00); | |
| ck_assert_int_eq(utf16_from_coding(0x1F600, &high, &low), | |
| SLURM_SUCCESS); | |
| ck_assert_int_eq(high, 0xD83D); | |
| ck_assert_int_eq(low, 0xDE00); | |
| /* round-trip: from_coding() then to_coding() returns the scalar */ | |
| ck_assert_int_eq(utf16_from_coding(0x1D11E, &high, &low), | |
| SLURM_SUCCESS); | |
| ck_assert_int_eq(utf16_to_coding(high, low, &utf), SLURM_SUCCESS); | |
| ck_assert_int_eq(utf, 0x1D11E); | |
| ck_assert_int_eq(utf16_from_coding(0x00E9, &high, &low), SLURM_SUCCESS); | |
| ck_assert_int_eq(utf16_to_coding(high, low, &utf), SLURM_SUCCESS); | |
| ck_assert_int_eq(utf, 0x00E9); | |
| /* invalid scalars are rejected with the utf_is_valid() error */ | |
| ck_assert_int_eq(utf16_from_coding(0x0, &high, &low), | |
| ESLURM_UTF_NULL_CODE); | |
| ck_assert_int_eq(utf16_from_coding(0xD800, &high, &low), | |
| ESLURM_UTF16_SURROGATE_CODE); | |
| ck_assert_int_eq(utf16_from_coding(0xE000, &high, &low), | |
| ESLURM_UTF_PRIVATE_CODE); | |
| ck_assert_int_eq(utf16_from_coding(0x10FFFF, &high, &low), | |
| ESLURM_UTF_NONCHARACTER_CODE); | |
| ck_assert_int_eq(utf16_from_coding(0x110000, &high, &low), | |
| ESLURM_UTF_INVALID_CODE); | |
| } | |
| END_TEST | |
| /* --------------------------------------------- is_utf16 surrogate macros */ | |
| START_TEST(test_utf16_surrogate_macros) | |
| { | |
| ck_assert(utf16_is_high_surrogate(0xD800)); | |
| ck_assert(utf16_is_high_surrogate(0xDBFF)); | |
| ck_assert(!utf16_is_high_surrogate(0xD7FF)); | |
| ck_assert(!utf16_is_high_surrogate(0xDC00)); | |
| ck_assert(utf16_is_low_surrogate(0xDC00)); | |
| ck_assert(utf16_is_low_surrogate(0xDFFF)); | |
| ck_assert(!utf16_is_low_surrogate(0xDBFF)); | |
| ck_assert(!utf16_is_low_surrogate(0xE000)); | |
| } | |
| END_TEST | |
| /* ----------------------------------------------- utf_read_encoding_schema() */ | |
| START_TEST(test_read_encoding_schema) | |
| { | |
| const utf8_t utf8[] = UTF8_BYTE_ORDER_MARK_SEQ; | |
| const utf8_t utf16be[] = UTF16BE_BYTE_ORDER_MARK_SEQ; | |
| const utf8_t utf16le[] = UTF16LE_BYTE_ORDER_MARK_SEQ; | |
| const utf8_t utf32be[] = UTF32BE_BYTE_ORDER_MARK_SEQ; | |
| const utf8_t utf32le[] = UTF32LE_BYTE_ORDER_MARK_SEQ; | |
| const utf8_t none[] = { 0x41, 0x42, 0x43, 0x44 }; | |
| int bytes = -1; | |
| ck_assert_int_eq(utf_read_encoding_schema(utf8, utf8 + sizeof(utf8), | |
| &bytes), | |
| UTF_8_ENCODING); | |
| ck_assert_int_eq(bytes, sizeof(utf8)); | |
| ck_assert_int_eq(utf_read_encoding_schema(utf16be, | |
| utf16be + sizeof(utf16be), | |
| &bytes), | |
| UTF_16BE_ENCODING); | |
| ck_assert_int_eq(bytes, sizeof(utf16be)); | |
| ck_assert_int_eq(utf_read_encoding_schema(utf16le, | |
| utf16le + sizeof(utf16le), | |
| &bytes), | |
| UTF_16LE_ENCODING); | |
| ck_assert_int_eq(bytes, sizeof(utf16le)); | |
| ck_assert_int_eq(utf_read_encoding_schema(utf32be, | |
| utf32be + sizeof(utf32be), | |
| &bytes), | |
| UTF_32BE_ENCODING); | |
| ck_assert_int_eq(bytes, sizeof(utf32be)); | |
| /* | |
| * The UTF-32LE BOM (FF FE 00 00) begins with the UTF-16LE BOM (FF FE); | |
| * it must still be detected as UTF-32LE. | |
| */ | |
| ck_assert_int_eq(utf_read_encoding_schema(utf32le, | |
| utf32le + sizeof(utf32le), | |
| &bytes), | |
| UTF_32LE_ENCODING); | |
| ck_assert_int_eq(bytes, sizeof(utf32le)); | |
| ck_assert_int_eq(utf_read_encoding_schema(none, none + sizeof(none), | |
| &bytes), | |
| UTF_UNKNOWN_ENCODING); | |
| ck_assert_int_eq(bytes, 0); | |
| /* too short to hold any BOM */ | |
| ck_assert_int_eq(utf_read_encoding_schema(utf8, utf8, &bytes), | |
| UTF_UNKNOWN_ENCODING); | |
| ck_assert_int_eq(bytes, 0); | |
| /* | |
| * Buffers holding a proper prefix of a BOM, which exercise the | |
| * sizeof(bom) <= bytes guards that keep each memcmp() inside the buffer. | |
| */ | |
| ck_assert_int_eq(utf_read_encoding_schema(utf8, utf8 + 1, &bytes), | |
| UTF_UNKNOWN_ENCODING); /* EF */ | |
| ck_assert_int_eq(bytes, 0); | |
| ck_assert_int_eq(utf_read_encoding_schema(utf8, utf8 + 2, &bytes), | |
| UTF_UNKNOWN_ENCODING); /* EF BB */ | |
| ck_assert_int_eq(bytes, 0); | |
| ck_assert_int_eq(utf_read_encoding_schema(utf32be, utf32be + 3, &bytes), | |
| UTF_UNKNOWN_ENCODING); /* 00 00 FE */ | |
| ck_assert_int_eq(bytes, 0); | |
| /* | |
| * A 3-byte prefix of the UTF-32LE BOM is too short for UTF-32LE but is | |
| * still a complete UTF-16LE BOM. | |
| */ | |
| ck_assert_int_eq(utf_read_encoding_schema(utf32le, utf32le + 3, &bytes), | |
| UTF_16LE_ENCODING); /* FF FE 00 */ | |
| ck_assert_int_eq(bytes, sizeof(utf16le)); | |
| } | |
| END_TEST | |
| /* ----------------------------------------------- classification macros */ | |
| START_TEST(test_is_utf8_newline) | |
| { | |
| /* Unicode 5.8: CR, LF, VT, FF, NEL, LS, PS */ | |
| ck_assert(utf8_is_newline(0x0A)); /* LF */ | |
| ck_assert(utf8_is_newline(0x0B)); /* VT */ | |
| ck_assert(utf8_is_newline(0x0C)); /* FF */ | |
| ck_assert(utf8_is_newline(0x0D)); /* CR */ | |
| ck_assert(utf8_is_newline(0x85)); /* NEL */ | |
| ck_assert(utf8_is_newline(0x2028)); /* LS */ | |
| ck_assert(utf8_is_newline(0x2029)); /* PS */ | |
| /* | |
| * Deliberate superset of 5.8: the macro also treats the C0 separators | |
| * FS/GS/RS/US as newlines, so U+001C..U+001F are simultaneously newline | |
| * and control (test_is_utf8_control). | |
| */ | |
| ck_assert(utf8_is_newline(0x1C)); /* FS */ | |
| ck_assert(utf8_is_newline(0x1D)); /* GS */ | |
| ck_assert(utf8_is_newline(0x1E)); /* RS */ | |
| ck_assert(utf8_is_newline(0x1F)); /* US */ | |
| ck_assert(!utf8_is_newline(0x41)); /* 'A' */ | |
| ck_assert(!utf8_is_newline(0x20)); /* space */ | |
| ck_assert(!utf8_is_newline(0x09)); /* tab */ | |
| ck_assert(!utf8_is_newline(0x1B)); /* ESC, below the FS..US run */ | |
| ck_assert(!utf8_is_newline(0x2027)); /* just below LS */ | |
| ck_assert(!utf8_is_newline(0x202A)); /* just above PS */ | |
| } | |
| END_TEST | |
| START_TEST(test_is_utf8_space) | |
| { | |
| /* Table 6-2 spaces (a representative span) */ | |
| ck_assert(utf8_is_space(0x20)); /* space */ | |
| ck_assert(utf8_is_space(0x09)); /* tab (treated as horizontal space) */ | |
| ck_assert(utf8_is_space(0xA0)); /* no-break space */ | |
| ck_assert(utf8_is_space(0x1680)); /* ogham space mark */ | |
| ck_assert(utf8_is_space(0x2000)); /* en quad */ | |
| ck_assert(utf8_is_space(0x200A)); /* hair space */ | |
| ck_assert(utf8_is_space(0x202F)); /* narrow no-break space */ | |
| ck_assert(utf8_is_space(0x205F)); /* medium mathematical space */ | |
| ck_assert(utf8_is_space(0x3000)); /* ideographic space */ | |
| /* deliberate superset of Table 6-2: these four are not spaces there */ | |
| ck_assert(utf8_is_space(0x180E)); /* mongolian vowel separator */ | |
| ck_assert(utf8_is_space(0x200B)); /* zero width space */ | |
| ck_assert(utf8_is_space(0x2800)); /* braille pattern blank */ | |
| ck_assert(utf8_is_space(0xFFA0)); /* halfwidth hangul filler */ | |
| ck_assert(!utf8_is_space(0x41)); | |
| ck_assert(!utf8_is_space(0x0A)); /* newline is not horizontal space */ | |
| ck_assert(!utf8_is_space(0x200C)); /* ZWNJ: control, not space */ | |
| ck_assert(!utf8_is_space(0x1FFF)); /* just below the en quad run */ | |
| } | |
| END_TEST | |
| START_TEST(test_is_utf8_control) | |
| { | |
| /* C0 controls (minus tab/newlines), DEL, C1 controls */ | |
| ck_assert(utf8_is_control(0x00)); | |
| ck_assert(utf8_is_control(0x08)); | |
| ck_assert(utf8_is_control(0x1F)); | |
| ck_assert(utf8_is_control(0x7F)); /* DEL */ | |
| ck_assert(utf8_is_control(0x80)); /* C1 */ | |
| ck_assert(utf8_is_control(0x9F)); /* C1 */ | |
| ck_assert(utf8_is_control(0x200C)); /* zero width non-joiner */ | |
| ck_assert(utf8_is_control(0x0E)); /* SO, low end of 0x0E..0x1F */ | |
| ck_assert(utf8_is_control(0x1C)); /* FS: control *and* newline */ | |
| ck_assert(utf8_is_control(0x200D)); /* zero width joiner */ | |
| ck_assert(utf8_is_control(0x2060)); /* word joiner */ | |
| ck_assert(utf8_is_control(0x206F)); /* nominal digit shapes */ | |
| ck_assert(utf8_is_control(0xFFF9)); /* interlinear annotation anchor */ | |
| ck_assert(utf8_is_control(0xFFFB)); /* interlinear annotation term. */ | |
| ck_assert(utf8_is_control(0x34F)); /* combining grapheme joiner */ | |
| ck_assert(utf8_is_control(0x61C)); /* arabic letter mark */ | |
| ck_assert(utf8_is_control(0x115F)); /* hangul jamo block */ | |
| ck_assert(utf8_is_control(0x1160)); /* hangul jungseong filler */ | |
| ck_assert(utf8_is_control(0x3164)); /* hangul filler */ | |
| ck_assert(utf8_is_control(0xE0001)); /* language tag (deprecated) */ | |
| ck_assert(!utf8_is_control(0x09)); /* tab */ | |
| ck_assert(!utf8_is_control(0x0A)); /* newline */ | |
| ck_assert(!utf8_is_control(0x41)); /* 'A' */ | |
| ck_assert(!utf8_is_control(0x20)); /* space */ | |
| ck_assert(!utf8_is_control(0xA0)); /* NBSP is a space */ | |
| ck_assert(!utf8_is_control(0x0D)); /* CR is a newline */ | |
| } | |
| END_TEST | |
| START_TEST(test_is_utf8_whitespace) | |
| { | |
| ck_assert(utf8_is_whitespace(0x20)); /* space */ | |
| ck_assert(utf8_is_whitespace(0x09)); /* tab */ | |
| ck_assert(utf8_is_whitespace(0x0A)); /* newline */ | |
| ck_assert(utf8_is_whitespace(0x3000)); /* ideographic space */ | |
| ck_assert(utf8_is_whitespace(0x2028)); /* line separator */ | |
| ck_assert(utf8_is_whitespace(0x1C)); /* newline half of the union */ | |
| ck_assert(utf8_is_whitespace(0x180E)); /* space half of the union */ | |
| ck_assert(!utf8_is_whitespace(0x41)); | |
| ck_assert(!utf8_is_whitespace(0x00)); | |
| ck_assert(!utf8_is_whitespace(0x200C)); /* control, neither half */ | |
| } | |
| END_TEST | |
| /* --------------------------------------------------- utf8_get_loggable() */ | |
| START_TEST(test_get_utf8_loggable) | |
| { | |
| /* Control Pictures substitutions */ | |
| ck_assert_int_eq(utf8_get_loggable(0x00), 0x2400); /* NUL */ | |
| ck_assert_int_eq(utf8_get_loggable(0x0A), 0x240A); /* LF */ | |
| ck_assert_int_eq(utf8_get_loggable(0x1F), 0x241F); /* US */ | |
| ck_assert_int_eq(utf8_get_loggable(0x20), 0x2420); /* SPACE */ | |
| ck_assert_int_eq(utf8_get_loggable(0x7F), 0x2421); /* DEL */ | |
| ck_assert_int_eq(utf8_get_loggable(0x85), UTF_RETURN_SYMBOL_CODE); | |
| /* | |
| * Bidi controls. An unsubstituted LRO/RLO in a log line reorders | |
| * everything after it, which is the Trojan-Source log-spoofing vector. | |
| * They are also the only table entries past U+0085, so they guard the | |
| * table staying sorted: the lookup breaks as soon as an entry exceeds | |
| * the code, so a reordering drops them to the control fallback below. | |
| */ | |
| ck_assert_int_eq(utf8_get_loggable(0x200E), 0x2AAA); /* LRM */ | |
| ck_assert_int_eq(utf8_get_loggable(0x200F), 0x2AAB); /* RLM */ | |
| ck_assert_int_eq(utf8_get_loggable(0x202A), 0x2AAA); /* LRE */ | |
| ck_assert_int_eq(utf8_get_loggable(0x202B), 0x2AAB); /* RLE */ | |
| ck_assert_int_eq(utf8_get_loggable(0x202C), 0x2AA4); /* PDF */ | |
| ck_assert_int_eq(utf8_get_loggable(0x202D), 0x2AAA); /* LRO */ | |
| ck_assert_int_eq(utf8_get_loggable(0x202E), 0x2AAB); /* RLO */ | |
| /* | |
| * The fallbacks for codes with no table entry. These are the only | |
| * producers of their respective constants. | |
| */ | |
| ck_assert_int_eq(utf8_get_loggable(0x2028), UTF_RETURN_SYMBOL_CODE); | |
| ck_assert_int_eq(utf8_get_loggable(0x2029), UTF_RETURN_SYMBOL_CODE); | |
| ck_assert_int_eq(utf8_get_loggable(0x00A0), UTF_SPACE_REPLACEMENT_CODE); | |
| ck_assert_int_eq(utf8_get_loggable(0x3000), UTF_SPACE_REPLACEMENT_CODE); | |
| ck_assert_int_eq(utf8_get_loggable(0x0080), | |
| UTF_CONTROL_REPLACEMENT_CODE); | |
| ck_assert_int_eq(utf8_get_loggable(0x009F), | |
| UTF_CONTROL_REPLACEMENT_CODE); | |
| ck_assert_int_eq(utf8_get_loggable(0x200C), | |
| UTF_CONTROL_REPLACEMENT_CODE); | |
| /* a normal printable scalar is returned unchanged */ | |
| ck_assert_int_eq(utf8_get_loggable(0x41), 0x41); | |
| ck_assert_int_eq(utf8_get_loggable(0x20AC), 0x20AC); | |
| /* an invalid scalar that is not in the substitution table -> U+FFFD */ | |
| ck_assert_int_eq(utf8_get_loggable(0xD800), UTF_REPLACEMENT_CODE); | |
| ck_assert_int_eq(utf8_get_loggable(0x110000), UTF_REPLACEMENT_CODE); | |
| } | |
| END_TEST | |
| /* -------------------------------------- utf_encoding_scheme_to_string */ | |
| START_TEST(test_encoding_scheme_to_string) | |
| { | |
| ck_assert(!xstrcmp(utf_encoding_scheme_to_string(UTF_8_ENCODING), | |
| "UTF-8")); | |
| ck_assert(!xstrcmp(utf_encoding_scheme_to_string(UTF_16BE_ENCODING), | |
| "UTF-16BE")); | |
| ck_assert(!xstrcmp(utf_encoding_scheme_to_string(UTF_16LE_ENCODING), | |
| "UTF-16LE")); | |
| ck_assert(!xstrcmp(utf_encoding_scheme_to_string(UTF_32BE_ENCODING), | |
| "UTF-32BE")); | |
| ck_assert(!xstrcmp(utf_encoding_scheme_to_string(UTF_32LE_ENCODING), | |
| "UTF-32LE")); | |
| ck_assert(!xstrcmp(utf_encoding_scheme_to_string(UTF_UNKNOWN_ENCODING), | |
| "UNKNOWN")); | |
| /* the lookup falls through to xassert(false) if a row goes missing */ | |
| ck_assert(!xstrcmp(utf_encoding_scheme_to_string(UTF_INVALID), | |
| "INVALID")); | |
| ck_assert(!xstrcmp(utf_encoding_scheme_to_string(UTF_INVALID_MAX), | |
| "INVALID")); | |
| } | |
| END_TEST | |
| /* --------------------------------------------------- string wrappers */ | |
| START_TEST(test_utf8_string_wrappers) | |
| { | |
| /* U+00E9 (2 bytes) followed by 'a': 3 bytes, 2 characters */ | |
| const utf8_t src[] = { 0xC3, 0xA9, 'a', 0 }; | |
| const utf8_t ascii[] = { 'a', 'b', 'c', 0 }; | |
| utf8_t *dup = utf8_dup(src); | |
| utf8_t *ndup = utf8_ndup(src, 2); | |
| utf8_t *split = utf8_ndup(src, 1); | |
| utf8_t *adup = utf8_dup(ascii); | |
| /* all three wrappers count BYTES, not characters */ | |
| ck_assert_int_eq(utf8_strlen(src), 3); | |
| ck_assert_int_eq(utf8_strlen(ascii), 3); | |
| ck_assert_ptr_nonnull(dup); | |
| ck_assert_int_eq(utf8_strlen(dup), 3); | |
| ck_assert(!memcmp(dup, src, 4)); | |
| /* n counts bytes, so this keeps the whole 2-byte character */ | |
| ck_assert_ptr_nonnull(ndup); | |
| ck_assert_int_eq(utf8_strlen(ndup), 2); | |
| ck_assert_int_eq(ndup[0], 0xC3); | |
| ck_assert_int_eq(ndup[1], 0xA9); | |
| /* n does not character-align: this truncates mid-character */ | |
| ck_assert_ptr_nonnull(split); | |
| ck_assert_int_eq(utf8_strlen(split), 1); | |
| ck_assert_int_eq(split[0], 0xC3); | |
| ck_assert(adup && !xstrcmp((const char *) adup, "abc")); | |
| xfree(dup); | |
| xfree(ndup); | |
| xfree(split); | |
| xfree(adup); | |
| } | |
| END_TEST | |
| /* --------------------------------------------------- codespace capstone */ | |
| /* | |
| * Exhaustive completeness check across the entire U+0001..U+10FFFF codespace: | |
| * every code point is encoded and decoded back. Valid scalars must round-trip | |
| * exactly; invalid scalars must be encoded as the U+FFFD replacement, which | |
| * itself decodes back to U+FFFD. | |
| */ | |
| START_TEST(test_codespace_roundtrip) | |
| { | |
| long valid = 0, invalid = 0; | |
| for (utf_code_t cp = 1; cp <= 0x10FFFF; cp++) { | |
| utf8_t buf[UTF8_CHAR_MAX_BYTES] = { 0 }; | |
| int wbytes = 0, rbytes = 0; | |
| utf_code_t rcode = 0; | |
| int vrc = utf_is_valid(cp); | |
| ck_assert_int_eq(utf8_write_character(cp, buf, &wbytes), | |
| SLURM_SUCCESS); | |
| ck_assert_msg(utf8_read_character(buf, buf + wbytes, &rcode, | |
| &rbytes, | |
| true) == SLURM_SUCCESS, | |
| "readback failed for U+%06" PRIX32, cp); | |
| ck_assert_int_eq(rbytes, wbytes); | |
| if (!vrc) { | |
| ck_assert_msg(rcode == cp, | |
| "roundtrip U+%06" PRIX32 | |
| " -> U+%06" PRIX32, | |
| cp, rcode); | |
| valid++; | |
| } else { | |
| ck_assert_msg(rcode == UTF_REPLACEMENT_CODE, | |
| "invalid U+%06" PRIX32 | |
| " not replaced (got U+%06" PRIX32 ")", | |
| cp, rcode); | |
| invalid++; | |
| } | |
| } | |
| printf("codespace roundtrip: %ld valid, %ld invalid scalars\n", valid, | |
| invalid); | |
| /* | |
| * The round-trip above classifies each code point with the same | |
| * utf_is_valid() the reader uses, so on its own it holds under any | |
| * policy, right or wrong. These totals are what make the sweep an | |
| * assertion about the policy itself, and they are exact: | |
| * | |
| * 2048 surrogates U+D800..U+DFFF | |
| * 66 noncharacters U+FDD0..U+FDEF (32) + U+nFFFE/U+nFFFF | |
| * for planes 0..16 (34) | |
| * 137468 private use U+E000..U+F8FF (6400) | |
| * + U+F0000..U+FFFFD (65534) | |
| * + U+100000..U+10FFFD (65534) | |
| * 9 reserved U+FFF0..U+FFF8 | |
| * ------ | |
| * 139591 invalid, and 1114111 - 139591 = 974520 valid. | |
| * | |
| * The ranges do not overlap: U+nFFFE/U+nFFFF sit above the end of every | |
| * private-use range, and U+FFF0..U+FFF8 is disjoint from both. | |
| */ | |
| ck_assert_int_eq(invalid, 139591); | |
| ck_assert_int_eq(valid, 974520); | |
| ck_assert_int_eq(valid + invalid, 0x10FFFF); | |
| } | |
| END_TEST | |
| extern int main(int argc, char **argv) | |
| { | |
| int failures; | |
| TCase *tcase = tcase_create("xutf"); | |
| Suite *suite = suite_create("xutf"); | |
| SRunner *sr = NULL; | |
| tcase_add_unchecked_fixture(tcase, setup, teardown); | |
| tcase_set_timeout(tcase, 120); | |
| tcase_add_test(tcase, test_read_ascii); | |
| tcase_add_test(tcase, test_read_two_byte); | |
| tcase_add_test(tcase, test_read_three_byte); | |
| tcase_add_test(tcase, test_read_four_byte); | |
| tcase_add_test(tcase, test_read_overlong); | |
| tcase_add_test(tcase, test_read_surrogate_bytes); | |
| tcase_add_test(tcase, test_read_above_max); | |
| tcase_add_test(tcase, test_read_out_of_range_lead); | |
| tcase_add_test(tcase, test_read_illegal_lead); | |
| tcase_add_test(tcase, test_read_truncated); | |
| tcase_add_test(tcase, test_read_maximal_subpart); | |
| tcase_add_test(tcase, test_read_policy_rejects); | |
| tcase_add_test(tcase, test_read_no_check_valid); | |
| tcase_add_test(tcase, test_read_empty_range); | |
| tcase_add_test(tcase, test_read_macro); | |
| tcase_add_test(tcase, test_write_valid); | |
| tcase_add_test(tcase, test_write_invalid_to_replacement); | |
| tcase_add_test(tcase, test_write_ascii_direct); | |
| tcase_add_test(tcase, test_is_utf_valid); | |
| tcase_add_test(tcase, test_utf16_to_coding); | |
| tcase_add_test(tcase, test_utf16_from_coding); | |
| tcase_add_test(tcase, test_utf16_surrogate_macros); | |
| tcase_add_test(tcase, test_read_encoding_schema); | |
| tcase_add_test(tcase, test_is_utf8_newline); | |
| tcase_add_test(tcase, test_is_utf8_space); | |
| tcase_add_test(tcase, test_is_utf8_control); | |
| tcase_add_test(tcase, test_is_utf8_whitespace); | |
| tcase_add_test(tcase, test_get_utf8_loggable); | |
| tcase_add_test(tcase, test_encoding_scheme_to_string); | |
| tcase_add_test(tcase, test_utf8_string_wrappers); | |
| tcase_add_test(tcase, test_codespace_roundtrip); | |
| suite_add_tcase(suite, tcase); | |
| sr = srunner_create(suite); | |
| srunner_run_all(sr, CK_VERBOSE); | |
| failures = srunner_ntests_failed(sr); | |
| srunner_free(sr); | |
| return failures; | |
| } |