Repository navigation
Conversation
Lua 5.1's string library classifies bytes with the C library, which
follows LC_CTYPE. Python sets LC_CTYPE from the environment, and in a
UTF-8 locale macOS counts 0xA0 as a space and 0xE0 as a letter; Latin-1
and Windows code-page locales also lower-case bytes above 0x7F. So %s,
%a and string.lower() cut or change UTF-8 characters ("à" is C3 A0) and
modules later fail with "string is not UTF-8" (see tatuylonen#301).
Wrap string.find/match/gmatch/gsub to write each character class as an
explicit ASCII byte set, and make lower()/upper() change only A-Z/a-z,
so results match the C locale (as on MediaWiki's servers) everywhere.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
|
|
||
| # Give the string library the C locale's character classes, whatever | ||
| # locale the process runs in (see the file's comment). | ||
| with (LUA_DIR / "_c_locale_string.lua").open(encoding="utf-8") as f: |
There was a problem hiding this comment.
This looks fine (although I won't pretend to get everything at a fine detail), but if the purpose of this is to emulate how stuff works on Linux, then we do not want to replace the original implementation when it's not necessary. That is, there should probably be a check here if we're running on Windows or MacOS, and only run the compatibility code if so. This should also prevent any problems on the Linux side if the implementation is not completely airtight, and reduce any performance penalties (even if very small).
|
This looks fine at a quick glance. Only thing I'd like to ask is to gate whether to use this compatibility code behind a check of if the system that is running is not compatible. Otherwise, if we're on Linux it is unnecessary. |
Lua 5.1's string library classifies bytes with the C library (
isspace(),isalpha(),tolower(), …), and that followsLC_CTYPE. Python setsLC_CTYPEfrom the environment at startup. In a UTF-8 locale macOS counts byte 0xA0 as a space and 0xE0 as a letter, and Latin-1 or Windows code-page locales lower-case bytes above 0x7F. "à" isC3 A0and "ム" isE3 83 A0, so%s,%aandstring.lower()cut or change them. Latermw.ustring/mw.textcalls then fail:This is the error in #301 (Windows,
string.lower()in a fr.wikipedia module). On macOS it hits ordinary en.wiktionary pages. 17 sample pages gave 179 such errors, 122 of them on à. Linux in a UTF-8 locale, like MediaWiki's servers, classifies only ASCII, so it never sees them.Change. A new
lua/_c_locale_string.lua, loaded once ininitialize_lua(), wrapsstring.find/match/gmatch/gsub. Each wrapper rewrites the pattern so that every class (%a %c %d %l %p %s %u %w %xand their complements, inside or outside[...]) becomes an explicit ASCII byte set. The pattern is read the waylstrlib.creads it.%bxy,%f[...], back-references and malformed patterns are kept, so Lua still reports the error. Rewritten patterns are cached.string.lower/upperchange onlyA-Z/a-zwhen the string has bytes above 0x7F. The string metatable shares the table, sos:find()is covered.This doesn't make
stringUnicode-aware. On any machine it gives exactly what the C locale gives, which is what MediaWiki returns. So modules behave as on the wiki, and Linux results don't change.Tests.
tests/test_lua_locale.pychecks all 256 bytes against every class (bare, in a set, negated) and lower/upper of every byte. It runs once in the current locale and once in a locale that misclassifies bytes, if the system has one (macOS: C.UTF-8; Linux: a Latin-1 locale; skipped otherwise). It also checks module calls that trim, case-change and split UTF-8 text, and that other pattern syntax survives. Before the change the new tests fail on macOS and on Linux with a Latin-1 locale (Debian,de_DE.ISO-8859-1). After it the suite passes (1009 tests) on macOS in the default locale and inLC_ALL=C, and on Linux. On Linux in C.UTF-8, results are the same with and without the change.Checks.
LC_ALL=C.LC_ALL=C.initialize_lua()and 0.05–0.5 µs per string call. Page expansion times were within noise.Worth your eye:
string.find(nil, "x")are still raised by the original functions, but the message may name the wrapper differently.[à], ranges like[\128-\255]) are copied unchanged, as before.%inside a set ([a-%]) is left untranslated, because lstrlib reads it oddly. Such a pattern keeps the locale behaviour.🤖 Generated with Claude Code