Istnieje możliwość takiego sprawdzenia. Sekwencje wielobajtowe w UTF-8 zostały tak dobrane, że kontrola błędu jest stosunkowo prosta bez żadnej dodatkowej tablicy znaków (btw. powodzenia z używaniem takowej - standard Unicode liczy miliony pozycji

). Będę podawać tutaj wartości bajtów zapisane binarnie, gdyż tam najlepiej to widać (x - dowolna wartość, koduje numer znaku).
0xxxxxxx - normalny znak ASCII z zakresu 0-127.
10xxxxxx - drugi lub każdy kolejny bajt "wyższych" znaków Unicode. Może być poprzedzony jedynie poprawną sekwencją startową UTF-8 lub innym 10xxxxxx. Jeśli jest przed nim coś innego, taki znak jest błędnie zakodowany.
Sekwencje startowe UTF-8 - kodują znaki od numerów 128 w górę.
110xxxxx - sekwencja dwubajtowa. Po niej musi następować JEDEN bajt 10xxxxxx.
1110xxxx - sekwencja trzybajtowa. Po niej muszą następować DWA bajty 10xxxxxx.
11110xxx - sekwencja czterobajtowa...
itd.
Sprawdzanie poprawności można wykonać przy pomocy prostego automatu skończonego (de facto wyrażenie regularne by wystarczyło, ale jak masz jeszcze po drodze poprawiać źle zakodowane znaki, to musisz własny zaklepać). Potrzebna Ci jest pętla, która jedzie po ciągu od początku do końca oraz jedna zmienna w której masz zapisany aktualny stan. Na Wikipedii możesz sobie doczytać, jak taki automat działa, bo tu szkoda na to miejsca. Opiszę jedynie tabelę przejść w postaci
(stan, aktualny znak) -> nowy stan
(domyślny, 0xxxxxxx) -> domyślny
(domyślny, 10xxxxxx) -> błąd_1
(domyślny, 110xxxxx) -> 2b_1
(domyślny, 1110xxxx) -> 3b_1
(domyślny, 11110xxx) -> 4b_1
(domyślny, 111110xx) -> 5b_1
(domyślny, 1111110x) -> 6b_1
Sprawdzanie sekwencji dwubajtowej:
(2b_1, 10xxxxxx) -> domyślny
(2b_1, xxxxxxxx) -> błąd_2
Sprawdzanie dla sekwencji trzybajtowej:
(3b_1, 10xxxxxx) -> 3b_2
(3b_1, xxxxxxxx) -> błąd_2
(3b_2, 10xxxxxx) -> domyślny
(3b_2, xxxxxxxx) -> błąd_2
Analogicznie konstruujesz stany dla sekwencji 4, 5 i 6-bajtowych. Jak dojdziesz do końca, to jest poprawne.
Taki automat sprawdza jedynie poprawność, ty go rozbudujesz o dodatkowy kod:
1. Przy przejściu do stanu "domyślny" przepisujesz znak do bufora
$poprawny zawierającego poprawiony ciąg.
2. Przy napotkaniu sekwencji startowej UTF-8, czyścisz bufor
$znakUTF i wprowadzasz tam odczytany bajt.
3. W stanach typu
3b_2,
2b_1 itd. czyli tych, co sprawdzają kolejne bajty znaku UTF-8 dodajesz je do bufora
$znakUTF. Jeśli to był ostatni bajt danej sekwencji, dodajesz
$znakUTF do bufora
$poprawny.
4. Przy wejściu do stanu
błąd_1 masz symbol z jakiegoś kodowania jednobajtowego (np. ISO-8859-2), który konwertujesz na odpowiadającą mu kilkubajtową sekwencję UTF-8. Sekwencję tę dodajesz do bufora
$poprawny i natychmiast przechodzisz do stanu
domyślny tak, żeby analiza mogła od kolejnego znaku trwać dalej.
5. Przy wejściu do stanu
błąd_2 masz sytuację, gdy sekwencja UTF-8 rozpoczęła się poprawnie, ale w którymś miejscu pojawił się błąd. Wtedy bierzesz każdy bajt ze
$znakUTF, traktujesz go jako jakiś symbol w kodowaniu jednobajtowym, konwertujesz na odpowiadającą mu sekwencję UTF-8 i dodajesz ją do bufora
$poprawny. Po zakończeniu od razu zmieniasz stan na
domyślny, by od następnego znaku rozpocząć dalej normalną analizę.
Oczywiście nic nie zagwarantuje Ci 100% poprawnej konwersji ciągu, w którym masz wymieszany UTF-8 i kodowania jednobajtowe. Jeśli ciąg znaków z przedziału 128-255 przypadkiem będzie tworzyć poprawną sekwencję znaku UTF-8, każdy algorytm, który nie próbuje zrozumieć czytanego tekstu nie zasygnalizuje tego jako błąd. Sytuacja taka jest mało prawdopodobna, ale możliwa.