News for hackers. Stuff that matters.
Re:Fast UTF-8 validation by Joker_vD on Tuesday October 20, 2020 @02:22PM (#24840691)
> finding the end of a string may place you into invalid memory if there are missing trailing bytes

I struggle to imagine such a function. If its signature is "const char * utf8end(const char * s, size_t len)", then the implementation is obviously "return s + len". If the len is missing then, uh, you can't find the end unless there is a terminator character, in which case the implementation is "return s + strlen(s)".

And if you mean finding the beginning of the last codepoint, then again, you simply go to the very last byte of the string and rewind back until you see a non-trailing byte (or you've run off the beginning of the string, or you've seen more than 4 bytes in which case you have an invalid string).

Re:Fast UTF-8 validation by rectang on Tuesday October 20, 2020 @03:32PM (#24841470)
> I struggle to imagine such a function.

The following function will read invalid memory if there are missing trailing bytes:

    int32_t
    read_last_code_point(const char *s, size_t len) {
        s += len; // place pointer at end of string
        while (!HEADER_BYTE(s)) { // read backwards until header byte found
            s--;
        }
        return DECODE_UTF8(s); // BOOM! May read beyond string end
    }
To fix it, you would either need to establish that the passed-in string was valid UTF-8, or you would need to add a bounds check to DECODE_UTF8, making it slower.

When you write string manipulation code, you come across this sort of issue all the time. In isolation, maybe you'd want to fix this function by adding the bounds check. But since you're going to see this problem everywhere, you'll probably solve it through an initial validation pass instead.

Re:Fast UTF-8 validation by svrb on Tuesday October 20, 2020 @03:40PM (#24841556)
Can you give an example of how one might make use of the last code point in a string?
Re:Fast UTF-8 validation by oconnor663 on Tuesday October 20, 2020 @04:43PM (#24842176)
In practice, this sort of thing might be most likely to come up with a chars iterator. The natural iteration logic is something like:

1) If position == end, the iterator is done. 2) Otherwise, read the next byte to determine the number of bytes in the next character. 3) Read all the bytes of the next character, convert them to a regular 32-bit code point, and emit that.

In the presence of invalid UTF-8, that iterator is broken. There needs to be an extra check in step 2.5: "Check that the number of bytes reported for the next character doesn't exceed the number of bytes remaining in the string." Iterating over the characters of a string is a pretty common thing to do, and that extra branch is not free.

Re:Fast UTF-8 validation by jart on Tuesday October 20, 2020 @09:55PM (#24844149)
If the natural logic is broken then you've misunderstood the thing's nature. UTF-8 behaves more like a communications stream that was shoehorned into the purpose of character arrays. If you think about it in that way, then decoding can be done simply and elegantly:

    #define bsr(u)          ((sizeof(int) * 8 - 1) ^ __builtin_clz(u))
    #define ThomPikeByte(x) ((x) & (((1 << ThomPikeMsb(x)) - 1) | 3))
    #define ThomPikeMsb(x)  (((x)&0xff) < 252 ? bsr(~(x)&0xff) : 1)

    void ThompsonPikeDecoder(const char *s) {
      unsigned c, w = 0, t = 0;
      do {
        c = *s++ & 0xff;
        if (0200 <= c && c < 0300) {
          w = w << 6 | c & 077;
        } else {
          if (t) {
            printf("%04x\n", w);
            t = 0;
          }
          if (c < 0200) {
            printf("%04x\n", c);
          } else {
            w = ThomPikeByte(c);
            t = 1;
          }
        }
      } while (c);
    }
That code generalizes to any 32-bit number. It's the full superset of intended behaviors including things like stream synchronization. It'll even decode numbers that were arbitrarily banned by the IETF e.g. \300\200. Most importantly, since it doesn't require a validation pass beforehand, does that mean it goes faster than the OP's code in praxis? D:
Re:Fast UTF-8 validation by Dylan16807 on Wednesday October 21, 2020 @03:57AM (#24845687)
I strongly question "doesn't require a validation pass".

1. Your code drops naked continuation bytes entirely.

2. If a valid multibyte character is followed by extraneous continuation bytes it will not decode correctly.

3. Using the wrong number of continuation bytes is a giant mess and can give you arbitrary numbers in multiple ways.

The second one especially is a behavior that's hard to defend.

Re:Fast UTF-8 validation by qppo on Tuesday October 20, 2020 @05:30PM (#24842544)
The check only needs to be done once, when the iterator is constructed. You can also zero pad the buffer by 3 bytes to avoid a bounds check if you really want.

This seems like a premature optimization, unless you're only iterating over tiny strings.

Re:Fast UTF-8 validation by oconnor663 on Tuesday October 20, 2020 @09:26PM (#24844019)
That's a good point. I know in the case of Rust in particular there are other invariants, like that each 32-bit code point is guaranteed to have a valid value. (The compiler knows this and may use unset bits to stash enum state or something like that.) But maybe in other languages without such strict validity guarantees, it's less of a big deal?
Re:Fast UTF-8 validation by rectang on Tuesday October 20, 2020 @04:05PM (#24841825)
I tried to simplify the example code for the sake of clarity.

While I acknowledge it would be unusual to provide a library API for finding the value of the last code point specifically, most string libraries provide something like a "codePointAt" function to return the code point at a specific offset. You'll run into the same problem with DECODE_UTF8 there.

    int32_t
    code_point_at(const char *s, size_t len, size_t offset) {
        const char *end = s + len;
        while (s < end && offset > 0) {
            // Move forward by 1-4 bytes depending on header byte value.
            s += UTF8_SKIP(s);
            offset--;
        }
        if (s >= end) {
            return -1;
        }
        return DECODE_UTF8(s); // BOOM! May read beyond string end
    }
Re:Fast UTF-8 validation by Joker_vD on Tuesday October 20, 2020 @04:18PM (#24841945)
The iteration count is at most 4 anyway, so we're arguing about nanoscopical performance difference. Just check the boundary.
Re:Fast UTF-8 validation by zamadatix on Tuesday October 20, 2020 @02:37PM (#24840857)
> If the len is missing then, uh, you can't find the end unless there is a terminator character, in which case the implementation is "return s + strlen(s)".

Sure, and if you do strlen(s) without checking if there is actually a NUL first then:

> finding the end of a string may place you into invalid memory if there are missing trailing bytes.

As was originally said. This doesn't mean it's impossible to make such functions but <rest of original comment>.

Re:Fast UTF-8 validation by shawxe on Tuesday October 20, 2020 @03:32PM (#24841463)
> Sure, and if you do strlen(s) without checking if there is actually a NUL first then:

>> finding the end of a string may place you into invalid memory if there are missing trailing bytes.

You're really losing me here. Either you have a NUL terminated string or you already know the length of the string or you have no way of determining the length of the string that exists in memory that it is safe for you to access and what you have is garbage.

Just because data is "valid UTF-8" doesn't make it safe to read. If the end of your string isn't marked and you don't know how long the string is already, it's over.

Re:Fast UTF-8 validation by zamadatix on Tuesday October 20, 2020 @03:53PM (#24841697)
Or you reserved the string ahead of time or it was reused and not zeroed properly or there is more than 1 null in the data and copying must be done with care.

Again, it's doable, or you can validate the string once (including more than "it's valid encoding") and make a ton of safe assumptions later instead of each thing that interacts with the string. UTF or not.

Re:Fast UTF-8 validation by a1369209993 on Tuesday October 20, 2020 @03:31PM (#24841454)
> if you do strlen(s) without checking if there is actually a NUL first

Then your problems have nothing to do with invalid UTF-8, because that issue occurs even in pure ASCII.

Re:Fast UTF-8 validation by labawi on Tuesday October 20, 2020 @03:09PM (#24841222)
Function finding end of a string that breaks on invalid utf8 would be silly and is a bad example.

A function that counts the number of characters/codepoints or maybe decodes utf8 -> ucs4, may wish to operate on entire codepoints instead of always checking for potential end of buffer mid-decoding. Or perhaps avoid handling of invalid codepoints at all.

Re:Fast UTF-8 validation by a1369209993 on Tuesday October 20, 2020 @04:57PM (#24842289)
> may wish to operate on entire codepoints instead of always checking for potential end of buffer mid-decoding

That sounds like the sort of reasoning that results in compiler bugs like:

  thing_t* nextp=p+1;
  // other declarations for this function
  if(!p) abort_thing("null pointer"); // optimised out
  /* CVE-20XX-#####: user-space code can compromise kernel $THING if it mmap()s $STUFF at address zero */
Re:Fast UTF-8 validation by naniwaduni on Tuesday October 20, 2020 @06:17PM (#24842896)
> A function that counts the number of characters/codepoints

Have you ever met a use case for the number of codepoints in a unicode string?

(One that didn't involve the moral equivalent of "because codepoints are what Twitter counts"?)

Re:Fast UTF-8 validation by rectang on Tuesday October 20, 2020 @06:38PM (#24843023)
When converting from UTF-8 to UTF-32, you need to know the number of codepoints so that you know how much memory to allocate.

I feel like you're hinting at something. What is it?

Re:Fast UTF-8 validation by naniwaduni on Wednesday October 21, 2020 @03:07AM (#24845424)
Sure. You could also multiply by 4, wasting at worst twice as much memory as optimal on pathological input.

But wait, why are you converting from utf-8 to utf-32? To iterate through? You don't need to count your codepoints for that, you can just advance through your utf-8 linearly. To index into it? You might as well index into byte offsets—sure, that makes some possible indices invalid, but so does the finite length of your buffer.

And what are you doing with the codepoints anyway? utf-32 turns out to be profoundly useless for pretty much every concrete use case, because its whole appeal, that its code units map 1:1 to code points, presupposes that there's something you'd want to do with the codepoints.

There isn't. Code points are practically orthogonal to all of: user-perceived characters, user-perceived graphical symbols, grapheme clusters, text width. They combine arbitrarily in ways that you cannot infer without knowing the semantics of every possible code point. There is no linguistically sensible text operation you can do at the codepoint level. They are completely useless to you.

The worst part is, there are a lot of operations you can do with codepoints that seem, to cursory inspection, to do something you want. But they're wrong, all of them, necessarily so, because you're working at a level of abstraction that does not have the information needed to do just about anything correctly, but the cases that are broken are ones where none of your developers even know what the correct result should be.

Re:Fast UTF-8 validation by ben509 on Tuesday October 20, 2020 @08:02PM (#24843586)
It's not very sporting to rule out making things fit as a use case for measuring something.

Even measuring the width of rendered text is done to make it fit some layout.

Re:Fast UTF-8 validation by naniwaduni on Wednesday October 21, 2020 @02:36AM (#24845296)
The width of rendered text is not measured in codepoints.
Re:Fast UTF-8 validation by iforgotpassword on Tuesday October 20, 2020 @04:35PM (#24842100)
If you pre-validate the string because maybe you do a lot of transformations and operations with it later, then any functions processing strings become simpler. If you have the start of a 3byte character you can just read the next two bytes and don't need to check if any of the two trailing bytes is actually a nullchar instead (in case of null-terminated strings) or whether pos+2<len. Because otherwise you'd do an oob read. Also you don't need to check if the trailing bytes of your char actually start with 10xxxxxx, you can just mask off the first two bits right away.