// Encoding and decoding UTF-8; defining the ustring type. // // See // // * Unicode standard, Sects. 2.5 and 3.9: // https://www.unicode.org/versions/Unicode15.1.0/ch02.pdf#G13708 // https://www.unicode.org/versions/Unicode15.1.0/ch03.pdf#G7404 // * RFC 2781: UTF-16, an encoding of ISO 10646 -- https://www.ietf.org/rfc/rfc2781.txt // * RFC 3629: UTF-8, a transformation format of ISO 10646 -- https://www.ietf.org/rfc/rfc3629.txt // * Unicode FAQ: https://www.unicode.org/faq/utf_bom.html // * Markus Kuhn's UTF-8 decoder stess tests: https://www.cl.cam.ac.uk/~mgk25/ucs/examples/UTF-8-test.txt // // I deliberately don't include s7.h in this module, // since I want to keep this generic (for general tidiness/orthogonality reasons, // rather than because I necessarily want to move these functions elsewhere). // This means that some out-of-memory errors, which should really result in // a return_beastie_error or error_exit (both of which rely on the global S7 variable), // simply return NULL with an error message if possible. // // The ustring_t type is intended as a way of storing Unicode strings // internally to this program, in a way which is reasonably efficient // to store and manipulate, compared with storing everying as UTF-8 // internally (which is implicitly mostly done by s7), or as lists of // s7 integers (which works, but feels terribly clumsy, and the weak // typing of which requires one to remember whether a list is 'just' a // list, or in fact a list of codepoints). It means that strings are // stored as 16 bits per codepoint (if everything's in the BMP) rather // than 64 bits (if we were using lists of integers, since s7_int is int64_t). // // The encoding of codepoints // here is almost identical to that of UTF-16, but isn't _quite_ the // same thing, since it does so using a sequence of uint16_t integers, // rather than a sequence of big- or little-endian bytes. // // The code here includes versions which do and don't use ICU. // The ICU API docs are at // https://unicode-org.github.io/icu-docs/apidoc/released/icu4c/index.html // // This file is part of Beastie // SPDX-FileCopyrightText: 2024 Norman Gray // SPDX-License-Identifier: BSD-2-Clause #if __GNUC__ // for stpncpy #define _XOPEN_SOURCE 700 #endif #include #include #include #include #include #include "unicode.h" #if HAVE_ICU #include #include #include #include #include #endif static int verbosity_ = 1; int unicode_verbosity(int increment) { if (increment > 0) { ++verbosity_; } else if (increment < 0 && verbosity_ > 0) { --verbosity_; } return verbosity_; } // Return a static string containing the // UCD version this build is based upon. const char* unicode_version() { #if HAVE_ICU #define VLEN 16 static char version_string[VLEN] = ""; if (version_string[0] == '\0') { UVersionInfo unicode_version; u_getUnicodeVersion(unicode_version); snprintf(version_string, VLEN, "%d.%d.%d", unicode_version[0], unicode_version[1], unicode_version[2]); } return version_string; #undef VLEN #else #include "misc/unicode/ucd/ucd_version.h" return ucd_version; #endif } // No, the following isn't thread-safe, but if we allocate memory here // we have to free it later, and this error message is // possibly/probably going to end up being passed to // return_beastie_error, which inevitably returns before freeing // anything. A way of getting round this would be to create and // return a scheme object, but that would require pulling s7.h in here // in a way that feels inelegant just now. Perhaps come back to this later. // // Since this error message is probably terminal, we seem likely to be // exiting soon anyway, so that this in-principle thread-unsafety // doesn't much matter. #define ERRMSG_BUF_LEN 1024 static char errmsg_buf[ERRMSG_BUF_LEN]; static const char* create_errmsg(const char* fmt, ...) { va_list ap; va_start(ap, fmt); vsnprintf(errmsg_buf, ERRMSG_BUF_LEN, fmt, ap); va_end(ap); return errmsg_buf; } #if 0 // a dynamic version, but not to be used... static const char* create_errmsg(const char* fmt, ...) { char* msg; va_list ap; va_start(ap, fmt); int status = vasprintf(&msg, fmt, ap); if (status < 0) { // out of memory! // // It would be good, here, to call error_exit from util.h. // Doing so, however, would be the only thing which connects // this module to S7, which in turn requires that // test/test-unicode.c include a definition of global S7. // That's not a terrible thing, but makes the overall network // of dependencies that bit more complicated, and in turn // slightly complicates the linking step in Linux (suddenly // GCC needs to include -lm for some reason!), so this seems // something we should avoid until there's a more pressing // need for it than this. fprintf(stderr, "Out of memory creating error message (%s) in unicode.c", fmt); return NULL; } va_end(ap); return msg; } #endif // Returned by decode_utf8 at EOF // (not a valid codepoint) const codepoint_t UNICODE_EOF = -1; // Returned by decode_utf8 when it can't decode a UTF-8 sequence // (also not a valid codepoint). const codepoint_t UNICODE_BAD_DECODE = -2; const codepoint_t UNICODE_REPLACEMENT_CHARACTER = 0xfffdU; #if HAVE_ICU inline int is_surrogate(const codepoint_t cp) { return U16_IS_SURROGATE(cp); } inline codepoint_t from_surrogate(const uint16_t* const p, const char** errmsg) { if (U16_IS_LEAD(p[0]) && U16_IS_TRAIL(p[1])) { return U16_GET_SUPPLEMENTARY(p[0], p[1]); } else { if (errmsg != NULL) *errmsg = create_errmsg("from_surrogate: bad call with U+%04x U+%04x", p[0], p[1]); return UNICODE_BAD_DECODE; } } codepoint_t decode_utf8(const unsigned char* bc, size_t len, unsigned char* nused, const char** errmsg) { int i = 0; codepoint_t rval; U8_NEXT(bc, i, len, rval); if (nused != NULL) *nused = i; if (rval < 0) { if (errmsg != NULL) *errmsg = "can't decode UTF-8"; rval = UNICODE_BAD_DECODE; } return rval; } const unsigned char* encode_utf8(const codepoint_t cp, int* len) { static unsigned char b[5]; int idx = 0; char is_error = 0; unsigned char* rval; assert (len != NULL); U8_APPEND(b, idx, 5, cp, is_error); if (is_error) { *len = 0; rval = NULL; } else { // the function isn't documented to return a NULL-terminated // string, but it seems wise to do so nonetheless b[idx] = '\0'; *len = idx; rval = b; } return rval; } #else // Tests whether a codepoint (as opposed to a UTF-16 code unit) is a surrogate. inline int is_surrogate(const codepoint_t cp) { return (cp & 0xf800) == 0xd800; // equivalent to the following (but a whole opcode faster! -- wheee!) //return cp >= 0xd800 && cp < 0xe000; } // Convert two bytes, containing UTF-16 surrogates, to a single codepoint. // The argument p is taken to be a two-element array. // We don't do any error-checking here (cf, RFC 2781, Sect.2.2), // because we presume that the content here was written only // by other functions in this module. // Since we don't protect the relevant memory, this means, of course, // that a mischievous client could interfere with the content of the // 's' field of the ustring_t object, producing potentially unwanted // effects. I don't plan to worry about this just now. static inline codepoint_t from_surrogate0(const uint16_t* const p) { return ((p[0] & 0x3ff) << 10 | (p[1] & 0x3ff)) + 0x10000; } codepoint_t from_surrogate(const uint16_t* const p, const char** errmsg) { codepoint_t rval; if (p[0] >= 0xd800 && p[0] < 0xdc00 && p[1] >= 0xdc00 && p[1] < 0xe000) { rval = from_surrogate0(p); } else { if (errmsg != NULL) *errmsg = "bad surrogate pair!"; rval = UNICODE_BAD_DECODE; } return rval; } // Decode a UTF-8 sequence of length 'len', returning a single codepoint. // // The buffer bc is of length len (which must be greater than 0). // This must be at least as long as the number of bytes implied by // the byte bc[0]. If nused is not NULL, then *nused is set to the // number of bytes converted, which will always be at least 1. // // If there is an error, then return UNICODE_BAD_DECODE // (not zero, since that's a valid codepoint which _might_ appear in some contexts). // If additionally errmsg is not NULL, then set *errmsg to a pointer // to an error message, which must be freed by the caller. // // On return (and if errmsg != NULL), either // (the return value is non-zero) XOR (*errmsg is non-zero). // // It should be the case that any non-zero codepoint that this // function returns is a codepoint that encode_utf8 will encode // without error. // // If nused is not NULL, then on return (1 <= *nused <= 4) // // Compare https://www.cl.cam.ac.uk/~mgk25/ucs/examples/UTF-8-test.txt codepoint_t decode_utf8(const unsigned char* bc, size_t len, unsigned char* nused, const char** errmsg) { const unsigned char* b = (const unsigned char*)bc; unsigned int nbytes = 0; codepoint_t rval = UNICODE_BAD_DECODE; if (errmsg != NULL) *errmsg = NULL; // default if (len < 1) { // this is an internal function: should this perhaps be an assertion instead? if (errmsg != NULL) *errmsg = create_errmsg("bad call to decode_utf8: len<1"); // ensure *nused is >0 on exit (artificial, but this conforms // to the guarantee in the function contract) nbytes = 1; goto bail_out; } if ((b[0] & 0x80) == 0) { // 0xxxxxxx nbytes = 1; } else if ((b[0] & 0xe0) == 0xc0) { // 110xxxxx nbytes = 2; } else if ((b[0] & 0xf0) == 0xe0) { // 1110xxxx nbytes = 3; } else if ((b[0] & 0xf8) == 0xf0) { // 11110xxx nbytes = 4; } else { if (errmsg != NULL) *errmsg = create_errmsg("invalid UTF-8 byte-0: 0x%x", b[0]); nbytes = 1; goto bail_out; } #if 0 // or verbosity >2? fprintf(stderr, "decoding [%zu]", len); for (int i=0; i len) { if (errmsg != NULL) *errmsg = create_errmsg("malformed UTF-8 byte 0x%x indicates more bytes than are available", b[0]); nbytes = len; goto bail_out; } for (int i=nbytes-1; i>0; i--) { if ((b[i] & 0xc0) != 0x80) { if (errmsg != NULL) *errmsg = create_errmsg("malformed UTF-8: bad continuation byte %d: 0x%x", i, b[i]); goto bail_out; } } // Check for overlong sequences -- that is, sequences which have // used more bytes to encode them than necessary. // Rather than checking for giveaway bit patterns, just base this // on the largest codepoints correctly encoded to a given length. // // Such sequences are not ill-formed as such (if I'm interpreteting things // correctly, but see the Unicode spec, ch.3, which makes some // rather opaque remarks on these lines), but they are non-minimal and // (according to Kuhn) should be rejected on security grounds. int overlong = 0; switch (nbytes) { case 1: rval = b[0]; break; case 2: rval = ((b[0] & 0x1f) << 6) | (b[1] & 0x3f); overlong = (rval < 0x80); break; case 3: rval = ((b[0] & 0x0f) << 12) | ((b[1] & 0x3f) << 6) | (b[2] & 0x3f); overlong = (rval < 0x800); break; default: // case 4 rval = ((b[0] & 0x07) << 18) | ((b[1] & 0x3f) << 12) | ((b[2] & 0x3f) << 6) | (b[3] & 0x3f); overlong = (rval < 0x10000); } // check rval... // (it might make sense to adjust these to use functions from // mycu.c, and prohibit characters of category Cx). if (overlong) { if (errmsg != NULL) *errmsg = create_errmsg("UTF-8 overlong sequence: U+%x encoded in %d bytes", rval, nbytes); rval = UNICODE_BAD_DECODE; } else if (is_surrogate(rval)) { if (errmsg != NULL) *errmsg = create_errmsg("UTF-8 decodes to surrogate: U+%x", rval); rval = UNICODE_BAD_DECODE; } else if (rval > 0x10ffff) { if (errmsg != NULL) *errmsg = create_errmsg("UTF-8 sequence out of range: U+%x", rval); rval = UNICODE_BAD_DECODE; } bail_out: assert(nbytes > 0); if (nused != NULL) *nused = nbytes; // error return behaviour (assuming errmsg is not NULL): // either we have a successful return and *errmsg is NULL, // or we have a failure return and *errmsg is non-NULL #if 0 if (! ((errmsg == NULL) || (rval == UNICODE_BAD_DECODE && *errmsg != NULL) || (rval != UNICODE_BAD_DECODE && *errmsg == NULL))) { fprintf(stderr, "ERROR: bc="); for (int i=0; i" : *errmsg), errmsg); } #endif assert((errmsg == NULL) || (rval == UNICODE_BAD_DECODE && *errmsg != NULL) || (rval != UNICODE_BAD_DECODE && *errmsg == NULL)); return rval; } // Encode a unicode codepoint into a static buffer, and return a // pointer to it, writing into *len the number of bytes thus encoded. // That is, this is not thread-safe. // // If the codepoint is a surrogate or out of the Unicode range, // then return NULL. // // len must be non-NULL. // This is an internal function: // if len is NULL, then produce an assertion error. const unsigned char* encode_utf8(const codepoint_t codepoint, int* len) { assert (len != NULL); static unsigned char b[5]; unsigned char* rval = b; if (codepoint < 0x80) { *len = 1; b[0] = codepoint; } else if (codepoint <= 0x7ff) { *len = 2; unsigned int y = (codepoint & 0x07c0) >> 6; unsigned int z = codepoint & 0x3f; b[0] = 0xc0 | y; b[1] = 0x80 | z; } else if (is_surrogate(codepoint)) { // this is a UTF-16 surrogate pair character *len = 0; rval = NULL; } else if (codepoint <= 0xffff) { *len = 3; unsigned int x = (codepoint & 0xf000) >> 12; unsigned int y = (codepoint & 0x0fc0) >> 6; unsigned int z = (codepoint & 0x3f); b[0] = 0xe0 | x; b[1] = 0x80 | y; b[2] = 0x80 | z; } else if (codepoint <= 0x10ffff) { *len = 4; unsigned int w = (codepoint & 0x1c0000) >> 18; unsigned int x = (codepoint & 0x03f000) >> 12; unsigned int y = (codepoint & 0x0fc0) >> 6; unsigned int z = (codepoint & 0x3f); b[0] = 0xf0 | w; b[1] = 0x80 | x; b[2] = 0x80 | y; b[3] = 0x80 | z; } else { // out of range *len = 0; rval = NULL; } // the function isn't documented to return a NULL-terminated // string, but it seems wise to do so nonetheless b[*len] = '\0'; return rval; } #endif // !HAVE_ICU // assert the _initial_ state of the reader static void assert_unicode_reader_valid(const unicode_reader* r) { // if 'in' is NULL, then buf must be too, and us must be non-NULL if (r->in == NULL) { // reading from a ustring #if !HAVE_ICU assert(r->buf == NULL); #endif assert(r->us != NULL); assert(r->filename == NULL); } else { // reading from a file #if !HAVE_ICU assert(r->buf != NULL); #endif assert(r->us == NULL); assert(r->filename != NULL); } // it's OK for r->buflen to be zero, for a zero-length input assert(r->idx == 0); assert(r->count == 0); assert(r->line_count == 1); assert(r->ascii_p == 0 || r->ascii_p == 1); // idx, count and line_count will change from these // values as the reader is used } // Create a unicode_reader from a file. // We assume, currently, that the file contents are encoded in UTF-8. unicode_reader* make_unicode_reader_file(const char* filename, size_t buflen, const char** errmsg) { unicode_reader* p = NULL; if ((p = malloc(sizeof(unicode_reader))) == NULL) { if (errmsg != NULL) { *errmsg = create_errmsg("make_unicode_reader_file: can't allocate memory"); } goto errexit; } // initialise to safe values, // in case we go to errexit and thence to unicode_reader_free early p->in = NULL; p->filename = NULL; p->us = NULL; #if HAVE_ICU if (filename) { if ((p->in = u_fopen(filename, "r", NULL, // locale "UTF_8")) == NULL) { // only UTF-8 for the mo if (errmsg != NULL) { *errmsg = create_errmsg("make_unicode_reader_file: can't open file <%s> (%s)", filename, strerror(errno)); } goto errexit; } } else { p->in = u_fadopt(stdin, NULL, "UTF_8"); filename = "*stdin*"; } #else p->buf = NULL; // safe initialiser if (filename) { if ((p->in = fopen(filename, "r")) == NULL) { if (errmsg != NULL) { *errmsg = create_errmsg("make_unicode_reader_file: can't open file <%s> (%s)", filename, strerror(errno)); } goto errexit; } } else { p->in = stdin; filename = "*stdin*"; } #endif p->us = NULL; size_t fnlen = strlen(filename); if ((p->filename = malloc(fnlen+1)) == NULL) { if (errmsg != NULL) { *errmsg = create_errmsg("make_unicode_reader_file: can't allocate filename buffer with %zu bytes", fnlen); } goto errexit; } memcpy(p->filename, filename, fnlen); p->filename[fnlen] = '\0'; #if !HAVE_ICU if ((p->buf = malloc(buflen)) == NULL) { if (errmsg != NULL) { *errmsg = create_errmsg("make_unicode_reader_file: can't allocate buffer of %zu bytes", buflen); } goto errexit; } p->buflen = buflen; // the initial values of len and idx don't matter, // as long as they're equal p->len = 0; #endif p->idx = p->count = 0; p->line_count = 1; // start on line 1 p->ascii_p = 0; p->pushed = 0; p->at_eof_p = 0; assert_unicode_reader_valid(p); return p; errexit: unicode_reader_free(p); return NULL; } // It might make more sense for this string argument to be unsigned char, // since it's intended to be bytes, but this function is in fact more // typically used for debugging and testing, where char* is more // convenient, so stick with that for now. // // We assume, currently, that the string is encoded in UTF-8. unicode_reader* make_unicode_reader_string(const unsigned char* bstr, const char** errmsg) { ustring_t us = make_ustring(errmsg); if (us == NULL) goto errexit; if (ustring_append_utf8(us, bstr, errmsg) == NULL) goto errexit; unicode_reader* ur = make_unicode_reader_ustring(us, errmsg); if (ur == NULL) goto errexit; // tell ur to take ownership of this ustring ur->our_ustring_p = 1; assert_unicode_reader_valid(ur); return ur; errexit: if (us != NULL) ustring_free(us); return NULL; } // Make a unicode_reader which takes data from the given ustring. // We do not take ownership of this string, // in the sense that our_ustring_p is left false. unicode_reader* make_unicode_reader_ustring(const ustring_t us, const char** errmsg) { unicode_reader* p = NULL; if ((p = malloc(sizeof(unicode_reader))) == NULL) { if (errmsg != NULL) { *errmsg = create_errmsg("make_unicode_reader_ustring: can't allocate memory"); } goto errexit; } #if !HAVE_ICU p->buf = NULL; p->buflen = p->len = us->len_; #endif p->in = NULL; p->us = us; p->filename = NULL; p->idx = 0; p->our_ustring_p = 0; p->count = 0; p->pushed = 0; p->line_count = 1; p->ascii_p = p->at_eof_p = 0; assert_unicode_reader_valid(p); return p; errexit: unicode_reader_free(p); return NULL; } void unicode_reader_free(unicode_reader* p) { if (p == NULL) return; // gcc gets upset about testing p->in uninitialised (silly gcc); // llvn doesn't like this warning, so complains about that (pernickety llvm) // (note: llvm does define __GNUC__, but gcc doesn't define __clang__) #pragma GCC diagnostic push #if __GNUC__ && !__clang__ #pragma GCC diagnostic ignored "-Wmaybe-uninitialized" #endif #if HAVE_ICU if (p->in != NULL) u_fclose(p->in); p->in = NULL; #else if (p->in != NULL) fclose(p->in); p->in = NULL; if (p->buf != NULL) free(p->buf); p->buf = NULL; #endif if (p->us != NULL && p->our_ustring_p) ustring_free(p->us); if (p->filename != NULL) free(p->filename); p->filename = NULL; #pragma GCC diagnostic pop free(p); } // Indicate the 'source' of a reader, by writing it in to a buffer. // This is a filename, or a "string". // If is_file_p is not NULL, then it is set to true if the source is a // file, or false if a string. // Returns the number of characters written to the buffer. const size_t unicode_reader_get_source(unicode_reader* ur, char* buf, size_t buflen, char* is_file_p) { char* str; char free_str_p = 0; if (ur->filename) { if (is_file_p != NULL) *is_file_p = 1; str = ur->filename; free_str_p = 0; } else { if (is_file_p != NULL) *is_file_p = 0; str = (char*)ustring_to_utf8(ur->us, NULL); free_str_p = 1; } // the following is a bit clumsy, since it might cut off a UTF-8 // sequence (no major harm; maybe come back to this later...) char* endp = stpncpy(buf, str, buflen-1); buf[buflen-1] = '\0'; // in case str is exactly buflen-1 long if (free_str_p) free(str); return endp - buf; } /* line Feed, vertical tab, form feed, carriage return, * next line, line separator, paragraph separator */ #define NEWLINE(c) ( \ ((c) >= 0xa && (c) <= 0xd) || \ (c) == 0x85 || (c) == 0x2028 || (c) == 0x2029 ) #define DEBUG_READ 0 const codepoint_t unicode_reader_next_cp(unicode_reader* p, const char** errmsg) { codepoint_t cp; if (p->at_eof_p) { cp = UNICODE_EOF; } else if (p->pushed) { cp = p->pushed; p->pushed = 0; } else if (p->in) { // reading from a file #if HAVE_ICU cp = u_fgetcx(p->in); #if DEBUG_READ fprintf(stderr, "read: U+%x = %c\n", cp, cp); #endif if (cp == U_EOF) { p->at_eof_p = 1; u_fclose(p->in); p->in = NULL; cp = UNICODE_EOF; } #else // !HAVE_ICU int nleft = p->len - p->idx; if (nleft < 4) { // We want there to be always at least four bytes in the // buffer, so decode_utf8 has all the bytes it will need. // The only time this won't be true is if we read fewer // than 4 bytes from the input, presuably at EOF if (nleft > 0) memmove(p->buf, &p->buf[p->idx], nleft); size_t nread = fread(&p->buf[nleft], 1, p->buflen - nleft, p->in); // if (nread == 0) { // fclose(p->in); // p->at_eof_p = 1; // p->in = NULL; // } #if DEBUG_READ fprintf(stderr, " nread=%zu -> [%.*s]\n", nread, (int)nread+nleft, p->buf); #endif p->len = nread + nleft; p->idx = 0; } #if DEBUG_READ > 1 fprintf(stderr, " -> p idx=%zu, len=%zu\n", p->idx, p->len); #endif if (p->idx >= p->len) { p->at_eof_p = 1; fclose(p->in); p->in = NULL; return UNICODE_EOF; // JUMP OUT } unsigned char nused; cp = decode_utf8(&p->buf[p->idx], p->len, &nused, errmsg); assert(nused > 0 && nused <= 4); #if DEBUG_READ > 1 fprintf(stderr, " -> cp U+%x = %c (nused=%d)\n", cp, cp, nused); #elif DEBUG_READ > 0 fprintf(stderr, "read: U+%x = %c\n", cp, cp); #endif p->idx += nused; #endif // HAVE_ICU // very simple-minded line-counting! (this double counts \r\n) if (NEWLINE(cp)) p->line_count++; // we've read one codepoint, represented in nused bytes p->count++; } else { // reading from ustring if (p->idx >= p->us->idx_) { p->at_eof_p = 1; cp = UNICODE_EOF; } else if (is_surrogate(p->us->s_[p->idx])) { cp = from_surrogate(p->us->s_ + p->idx, errmsg); p->idx += 2; } else { cp = p->us->s_[p->idx]; p->idx++; } p->count++; } return cp; } void push_codepoint(unicode_reader* p, codepoint_t cp) { // if there is already a codepoint pushed, then simply overwrite it, // without signalling an error (provisionally) p->pushed = cp; } #define VALID_USTRING(us) assert(us != NULL \ && us->s_ != NULL \ && us->alloc_ > 0 \ && us->len_ <= us->idx_ \ && us->idx_ <= us->alloc_) // Create a new ustring object. // If memory can't be allocated, // then save an error message to *errmsg (if it is non-NULL), // and return NULL. ustring_t make_ustring(const char** errmsg) { ustring_t us = malloc(sizeof(struct ustring_s)); if (us == NULL) { if (errmsg != NULL) *errmsg = create_errmsg("make_ustring: can't allocate space for make_ustring"); return NULL; } us->len_ = us->idx_ = 0; us->alloc_ = 16; ustring_cache_store_set(us, NULL); if ((us->s_ = malloc(us->alloc_ * sizeof(UChar))) == NULL) { if (errmsg != NULL) *errmsg = create_errmsg("make_ustring: can't allocate space for s_"); free(us); return NULL; } VALID_USTRING(us); return us; } void ustring_free(ustring_t us) { if (us == NULL) return; // for pragma notes, see above #pragma GCC diagnostic push #if __GNUC__ && !__clang__ #pragma GCC diagnostic ignored "-Wmaybe-uninitialized" #endif if (us->s_ != NULL) free(us->s_); us->s_ = NULL; ustring_cache_store_set(us, NULL); #pragma GCC diagnostic pop free(us); } // Reset a ustring's contents, without doing any allocations or deallocations void ustring_reset(ustring_t us) { us->idx_ = us->len_ = 0; ustring_cache_store_set(us, NULL); VALID_USTRING(us); } int ustring_length(ustring_t us) { return us->len_; } // Add a cache to ustrings. // // This is implemented in ustrings, but used by higher layers to cache // things derived from a ustring. The cache has to be implemented there, // so that it can be invalidated on any changes to the ustring. // // Yes, this slightly smacks of premature optimisation, but heads off the // recurring temptation to expose bstrings to the rest of the code (and in // particular the substantial attempt to use those within author-list // parsing, backed out from), by making it reasonable to pass ustrings back // and forth and implicitly reparse them. const void* ustring_cache_store_get(ustring_t us) { return us->cache_store_; } void ustring_cache_store_set(ustring_t us, const void* store) { us->cache_store_ = store; } //// Collation and locales #if HAVE_ICU static char* current_locale_ = NULL; static UCollator* current_collator_ = NULL; // Determine if two ustrings are equal, in a locale-sensitive way. // This could be made more sophisticated -- what does and doesn't // count as equal in (for example) normalisation terms. A whole world // of complication opens up... // // This doesn't support the USTRING_EQUAL_COLLAPSE_REPLACEMENTS flag // that the non-ICU version does. int ustring_equal(ustring_t us1, ustring_t us2, byte_t flags) { return ucol_strcoll(current_collator_, us1->s_, us1->idx_, us2->s_, us2->idx_) == UCOL_EQUAL; } int ustring_lt(ustring_t us1, ustring_t us2) { return ucol_strcoll(current_collator_, us1->s_, us1->idx_, us2->s_, us2->idx_) == UCOL_LESS; } const char* unicode_get_locale(void) { return current_locale_; } // Set a new locale. // Return non-zero on error. // On error, and if errmsg is not NULL, then *errmsg is filled with an error message // which should be freed by the caller. int unicode_set_locale(const char* locale, char** errmsg) { UErrorCode status = U_ZERO_ERROR; char normalised_locale[ULOC_FULLNAME_CAPACITY]; uloc_getName(locale, normalised_locale, ULOC_FULLNAME_CAPACITY, &status); if (U_FAILURE(status)) { if (errmsg == NULL) { fprintf(stderr, "Can't get uloc_getName: %s\n", u_errorName(status)); } else { const size_t buflen = 64; *errmsg = malloc(buflen); snprintf(*errmsg, buflen, "Can't get uloc_getName: %s\n", u_errorName(status)); } return 1; } // The documentation at https://unicode-org.github.io/icu/userguide/locale/ // suggests that uloc_canonlicalize does 'level 2' // canonicalisation, specifically mentioning "C" -> POSIX as one // of the possible changes. But this specific one doesn't seem to // happen in fact. I'm not sure if I'm using this incorrectly. // char canonicalised_locale[ULOC_FULLNAME_CAPACITY]; // uloc_canonicalize(locale, canonicalised_locale, ULOC_FULLNAME_CAPACITY, &status); // fprintf(stderr, "locales: %s -> %s -> %s\n", // locale, normalised_locale, canonicalised_locale); UCollator* new_collator = ucol_open(normalised_locale, &status); int rval; // TODO: implement some sort of stack if (new_collator != 0) { if (current_collator_ != NULL) ucol_close(current_collator_); current_collator_ = new_collator; if (current_locale_ != NULL) free(current_locale_); current_locale_ = strdup(normalised_locale); // for debugging... UErrorCode errorCode = U_ZERO_ERROR; const char* actual_locale = ucol_getLocaleByType(current_collator_, ULOC_VALID_LOCALE, &errorCode); if (verbosity_ > 1) { fprintf(stderr, "unicode_set_locale: set locale to %s (normalised %s; valid %s", locale, normalised_locale, actual_locale); actual_locale = ucol_getLocaleByType(current_collator_, ULOC_ACTUAL_LOCALE, &errorCode); fprintf(stderr, "; actual %s)\n", actual_locale); // How to get information about locales... // UChar buf[ULOC_FULLNAME_CAPACITY]; // char obuf[ULOC_FULLNAME_CAPACITY]; // uloc_getDisplayLanguage(normalised_locale, NULL, // buf, ULOC_FULLNAME_CAPACITY, // &status); // fprintf(stderr, " language: %s\n", // u_strToUTF8(obuf, ULOC_FULLNAME_CAPACITY, NULL, // buf, -1, &errorCode)); // uloc_getDisplayVariant(normalised_locale, NULL, // buf, ULOC_FULLNAME_CAPACITY, // &status); // fprintf(stderr, " variant: %s\n", // u_strToUTF8(obuf, ULOC_FULLNAME_CAPACITY, NULL, // buf, -1, &errorCode)); // fprintf(stderr, " keywords:\n"); // UEnumeration* key_enum = uloc_openKeywords(normalised_locale, &status); // if (key_enum == NULL) { // fprintf(stderr, " none\n"); // } else { // const char* key; // while ((key = uenum_next(key_enum, NULL, &status)) != NULL) { // fprintf(stderr, " %s\n", key); // } // uenum_close(key_enum); // } } rval = 0; } else { if (errmsg == NULL) { // best we can do... fprintf(stderr, "Unable to create a ICU collator!: %s", u_errorName(status)); } else { const size_t buflen = 64; // guess; probably big enough *errmsg = malloc(buflen); snprintf(*errmsg, buflen, "Unable to create a ICU collator!: %s", u_errorName(status)); } rval = 1; } return rval; } #else static int ustring_equal_collapse_replacements(ustring_t s1, ustring_t s2) { int result = -1; size_t i1, i2; for (i1=0, i2=0; result<0; i1++, i2++) { if (i1 == s1->len_ || i2 == s2->len_) { // at the end of one of the strings result = (i1 == s1->len_ && i2 == s2->len_); } else { if (s1->s_[i1] == UNICODE_REPLACEMENT_CHARACTER) { while (i1+1 < s1->len_ && s1->s_[i1+1] == UNICODE_REPLACEMENT_CHARACTER) i1++; } if (s2->s_[i2] == UNICODE_REPLACEMENT_CHARACTER) { while (i2+1 < s2->len_ && s2->s_[i2+1] == UNICODE_REPLACEMENT_CHARACTER) i2++; } if (s1->s_[i1] != s2->s_[i2]) { result = 0; } } } return result; } // The only currently recognised flag is // USTRING_EQUAL_COLLAPSE_REPLACEMENTS. If set, this collapses // sequences of replacement characters so "aXXXb" and "aXb" would test // as equal (because both are legitimate responses to an invalid // sequence). This is mostly to support tests. // // More flags might emerge, as we become more Unicode-sensitive. int ustring_equal(ustring_t s1, ustring_t s2, byte_t flags) { int result; if (flags & USTRING_EQUAL_COLLAPSE_REPLACEMENTS) { result = ustring_equal_collapse_replacements(s1, s2); } else if (s1->len_ == s2->len_ && s1->idx_ == s2->idx_) { result = 1; for (size_t i=0; iidx_; i++) { if (s1->s_[i] != s2->s_[i]) { //fprintf(stderr, " comparing failed at idx %zu: %d != %d\n", i, s1->s[i], s2->s[i]); result = 0; break; } } } else { result = 0; } return result; } // Return 1 if the first ustring is ordered before the second // Note: this comparison is _not_ Unicode-sensitive! int ustring_lt(ustring_t us1, ustring_t us2) { int i=0; uint16_t* cp1 = us1->s_; uint16_t* cp2 = us2->s_; while (1) { if (i == us1->idx_) return (us1->idx_ < us2->idx_); if (i == us2->idx_) return 0; if (*cp1 == *cp2) { cp1++; cp2++; i++; } else { return (*cp1 < *cp2); } } } int unicode_set_locale(const char* locale, char** errmsg) { // no-op return 0; } const char* unicode_get_locale(void) { return ""; } #endif //// Other string functions // Given a ustring_t, call the function f() on each codepoint. // The function will only be called on BMP codepoints, and must map BMP to BMP. // // Given that restriction, and the fact we want to map this function in place, // it seems simpler to implement this on the underlying UChar string. ustring_t ustring_map_func(ustring_t us, codepoint_t (*f)(codepoint_t)) { for (size_t i=0; ilen_; i++) { if (is_surrogate(us->s_[i])) { i++; } else { us->s_[i] = (*f)(us->s_[i]); } } ustring_cache_store_set(us, NULL); return us; } // Append a codepoint to the string, and return the ustring argument. // If the codepoint is not a unicode character // (ie, greater than U+10FFFF, // or is a surrogate, // or is the last two codepoints in a plane (ie, U+_fffe or U+_ffff), // or in range U+fdd0..U+fdef ('intended for process-internal uses'), // or if we can't allocate memory, // then save an error message to *errmsg (if it is non-NULL) and return NULL. ustring_t ustring_append_cp(ustring_t us, codepoint_t cp, const char** errmsg) { VALID_USTRING(us); if (us->idx_ > us->alloc_-2) { us->alloc_ *= 2; if ((us->s_ = realloc(us->s_, us->alloc_*sizeof(uint16_t))) == NULL) { if (errmsg != NULL) *errmsg = create_errmsg("Can't realloc ustring to %z units", us->alloc_); return NULL; } } #if HAVE_ICU if (U_IS_UNICODE_CHAR(cp)) { U16_APPEND_UNSAFE(us->s_, us->idx_, cp); } else { if (errmsg != NULL) *errmsg = create_errmsg("ustring_append_cp: can't add out-of-range codepoint U+%x (ignored)", cp); return NULL; } #else if (! U_IS_UNICODE_CHAR(cp)) { if (errmsg != NULL) *errmsg = create_errmsg("ustring_append_cp: can't add non-character U+%x (ignored)", cp); return NULL; } else if (cp < 0x10000) { us->s_[us->idx_++] = cp; } else { uint32_t cpx = cp - 0x10000; us->s_[us->idx_] = 0xd800 | (cpx >> 10); us->s_[us->idx_+1] = 0xdc00 | (cpx & 0x3ff); us->idx_ += 2; } #endif us->len_++; ustring_cache_store_set(us, NULL); VALID_USTRING(us); return us; } // Append a UTF-8 encoded string to the ustring. // Return the us argument on success, or NULL // (with an error in *errmsg) on error. ustring_t ustring_append_utf8(ustring_t us, const byte_t* s, const char** errmsg) { VALID_USTRING(us); size_t slen = strlen((const char*)s); if (slen == 0) { return us; } else { return ustring_append_utf8_with_length(us, s, slen, errmsg); } } ustring_t ustring_append_utf8_with_length(ustring_t us, const byte_t* s, const size_t slen, const char** errmsg) { if (slen == 0) return us; #if HAVE_ICU UErrorCode status = U_ZERO_ERROR; int32_t destLength; // number of code units written to the destination (void) u_strFromUTF8WithSub(us->s_ + us->idx_, us->alloc_ - us->idx_, &destLength, (const char*)s, slen, // length of s UNICODE_REPLACEMENT_CHARACTER, NULL, &status); if (U_SUCCESS(status)) { // We must increment us->idx_ by destLength, // but increment us->len_ by the number of codepoints // represented, which requires us to count the number of // surrogates here. int32_t endidx = us->idx_ + destLength; int nsurrogate_pairs = 0; for (int i=us->idx_; is_[i])) { nsurrogate_pairs++; i++; // skip over the second } } us->idx_ += destLength; us->len_ += destLength - nsurrogate_pairs; ustring_cache_store_set(us, NULL); } else if (status == U_BUFFER_OVERFLOW_ERROR) { // destLength contains the required size while (us->alloc_ < us->idx_ + destLength) us->alloc_ *= 2; if ((us->s_ = realloc(us->s_, us->alloc_*sizeof(UChar))) == NULL) { if (errmsg != NULL) *errmsg = create_errmsg("ustring_append_utf8: can't realloc ustring to %z units", us->alloc_); return NULL; } // recurse with larger buffer return ustring_append_utf8_with_length(us, s, slen, errmsg); } else if (status == U_INVALID_CHAR_FOUND) { // this probably shouldn't happen, since we're using // u_strFromUTF8WithSub ustring_append_cp(us, UNICODE_REPLACEMENT_CHARACTER, errmsg); } else { if (errmsg != NULL) *errmsg = create_errmsg("ustring_append_utf8: unexpected error: %s", u_errorName(status)); return NULL; } #else size_t idx = 0; while (idx < slen) { unsigned char nused; codepoint_t cp = decode_utf8(&s[idx], slen, &nused, errmsg); //ustring_append_cp(us, cp, errmsg); // ...even if it's UNICODE_BAD_DECODE if (cp == UNICODE_BAD_DECODE) { ustring_append_cp(us, UNICODE_REPLACEMENT_CHARACTER, errmsg); } else { ustring_append_cp(us, cp, errmsg); } idx += nused; } #endif VALID_USTRING(us); return us; } // Append the uchars array to the ustring. // The argument n_cp is the number of codepoints in the string, // and n_units is the number of code units, which will be more than // n_cp if some of the codepoints occupy two code-units. ustring_t ustring_append_uchars(ustring_t us1, UChar* uchars, size_t n_cp, // number of codepoints in the UChar string size_t n_units, // number of code units, >= n_cp const char** errmsg) { VALID_USTRING(us1); #if 0 // No: don't do this (why did I think this was needed?) // If either of n_cp or n_units is zero, then count to the first null // in uchars. if (n_cp == 0 || n_units == 0) { #if HAVE_ICU n_units = u_strlen(uchars); n_cp = u_countChar32(uchars, -1); #else n_units = 0; for (n_cp=0; uchars[n_cp] != 0; n_cp++) { if (is_surrogate(uchars[n_cp])) n_units++; n_units++; } #endif } #endif if (n_units < n_cp) { if (errmsg != NULL) *errmsg = create_errmsg("ustring_append_chars: bad call: fewer code-units (%d) than codepoints (%d)", n_units, n_cp); return NULL; } size_t newidx = us1->idx_ + n_units; if (newidx > us1->alloc_-2) { while (newidx > us1->alloc_-2) us1->alloc_ *= 2; if ((us1->s_ = realloc(us1->s_, us1->alloc_*sizeof(UChar))) == NULL) { if (errmsg != NULL) *errmsg = create_errmsg("Can't realloc ustring to %z units", us1->alloc_); return NULL; } } memcpy(&us1->s_[us1->idx_], uchars, n_units*sizeof(UChar)); us1->idx_ += n_units; us1->len_ += n_cp; ustring_cache_store_set(us1, NULL); VALID_USTRING(us1); return us1; } ustring_t ustring_append_ustring(ustring_t us1, ustring_t us2, const char** errmsg) { VALID_USTRING(us1); VALID_USTRING(us2); return ustring_append_uchars(us1, us2->s_, us2->len_, us2->idx_, errmsg); } // Return the codepoint at index in the ustring. // Return 0 if the index is out of range. codepoint_t ustring_ref(ustring_t us, size_t refidx) { if (refidx < 0 || refidx >= us->len_) { return 0; } else if (us->len_ == us->idx_) { // there are no codepoints requiring two UTF-16 code units return us->s_[refidx]; } else { uint16_t* a = us->s_; size_t i = 0; // count indexes into a[] for (size_t charnum=0; // count characters charnum= 0 && end < start)) { if (errmsg != NULL) *errmsg = create_errmsg("ustring_substring: bad call: start=%d must be <= end=%d", start, end); return NULL; } int copy_cp, copy_codeunit; if (end < 0 || end > us->len_) { // silently take end beyond EOS to be 'all of the string' copy_cp = us->len_ - start; copy_codeunit = us->idx_ - start; } else { copy_cp = copy_codeunit = end - start; if (us->len_ != us->idx_) { // there are some codepoints requiring two UTF-16 code units: // increment copy_codeunit by the number of such characters for (int i=start; is_[i])) { copy_codeunit++; i++; // jump over the other surrogate } } } } // fprintf(stderr, "ustring_substring: start=%d end=%d copy_cp=%d copy_codeunit=%d\n", // start, end, copy_cp, copy_codeunit); ustring_t res = make_ustring(NULL); return ustring_append_uchars(res, &us->s_[start], copy_cp, // no. codepoints copy_codeunit, // no. code units NULL); } // Encode the ustring in UTF-8; the result is zero-terminated. // The returned string must be freed after use. // If we run out of memory, then create an error message in *errmsg // (if non-NULL) and return NULL. #if HAVE_ICU const byte_t* ustring_to_utf8(ustring_t us, const char** errmsg) { size_t destlen = us->idx_ + us->idx_/2; // idx*1.5 (rough estimate) if (destlen < 2) destlen = 2; // oops, idx_==0, so ensure destlen>=2 char* rval; if ((rval = malloc(destlen * sizeof(char))) == NULL) { if (errmsg != NULL) *errmsg = create_errmsg("ustring_to_utf8: unable to allocate %z bytes", destlen); return NULL; } int32_t actualDestLength = 0; UErrorCode status = U_ZERO_ERROR; // the destlen-1 is to ensure we have space to null-terminate // at rval[actualDestLength] u_strToUTF8(rval, destlen-1, &actualDestLength, us->s_, us->idx_, &status); if (U_FAILURE(status)) { if (status == U_BUFFER_OVERFLOW_ERROR) { // We underestimated destlen above. // No problem: try again with the returned actualDestLength. destlen = actualDestLength+1; // +1 for zero-termination if ((rval = realloc(rval, destlen * sizeof(char))) == NULL) { if (errmsg != NULL) *errmsg = create_errmsg("ustring_to_utf8: unable to reallocate %z bytes", destlen); return NULL; } // try again... status = U_ZERO_ERROR; u_strToUTF8(rval, destlen, &actualDestLength, us->s_, us->idx_, &status); if (U_FAILURE(status)) { if (errmsg != NULL) *errmsg = create_errmsg("ustring_to_utf8: unexpected realloc error: %s", u_errorName(status)); return NULL; } } else { if (errmsg != NULL) *errmsg = create_errmsg("ustring_to_utf8: unexpected error: %s", u_errorName(status)); return NULL; } } rval[actualDestLength] = '\0'; return (byte_t*)rval; } #else const byte_t* ustring_to_utf8(ustring_t us, const char** errmsg) { size_t reqlen = 0; for (int i=0; iidx_; i++) { if (us->s_[i] < 0x80) { reqlen += 1; } else if (us->s_[i] < 0x800) { reqlen += 2; } else if (is_surrogate(us->s_[i])) { // surrogate (should be a high-surrogate) reqlen += 4; i++; // jump over low-surrogate } else { reqlen += 3; } } byte_t* b = malloc(reqlen + 1); if (b == NULL) { if (errmsg != NULL) *errmsg = create_errmsg("ustring_to_utf8: Unable to allocate %z bytes", reqlen); return NULL; } size_t bi = 0; size_t ui = 0; while (ui < us->idx_) { codepoint_t cp; if (is_surrogate(us->s_[ui])) { cp = from_surrogate0(&us->s_[ui]); ui += 2; } else { cp = us->s_[ui]; ui++; } int len; const byte_t* b8 = encode_utf8(cp, &len); memcpy(&b[bi], b8, len); bi += len; } b[bi] = 0; assert(bi == reqlen); return b; } #endif // Initialise this module. // Return zero on success. // On error, and if errmsg is not NULL, fill *errmsg with an error message, // which must be freed afterwards. int initialise_unicode_module(char** errmsg) { #if HAVE_ICU if (current_collator_ != NULL) return 0; // idempotent return unicode_set_locale(getenv("BEASTIE_LOCALE"), // env is NULL if not defined errmsg); #else return 0; #endif }