Part of #41: the comments of `benilla-ui` (the Lua UI engine, its FrameXML loader and its widgets) are rewritten to the rule in `docs/METHOD.md`, which is what the code does and the reference fact behind it, in a line. 43,140 comment lines become 15,161. - **The mechanical strip.** The citations that were only a private record number go first, and each edit is shown to be a pure removal. - **Wrong facts corrected.** The rewrite corrects the comments that stated wrong facts. Among them: - `0x64a480` is the case-sensitive compare (a byte `repz cmpsb`), and `0x64a4c0` the case-insensitive one. - A frame's id sits at `+0xb0`, not `+0xb4`. `EnableKeyboard`/`IsKeyboardEnabled` are `0x776ec0`/`0x776f90`. - A region's unpinned edge has no fallback to its owner's rect: the owner supplies only the scale. - `$parent` expands to the nearest named ancestor, or "Top" when none is named (`0x76c5b0`), not simply to the parent. - A StatusBar fills from its internal normalised fraction, not from `GetValue()`. - The addresses of `StartDuel` and `StartDuelUnit` were swapped. - Many "untraced" notes now give the traced reference fact: the drag threshold, the per-grip sizing edges, the empty-`autoFocus` skip, and `SetTexCoordModifiesRect`'s default. - Stock FrameXML line citations are checked against the stock 1.12 files and corrected. `ref-` file names become the stock file names. - **Deviation labels.** "Deviation:" now marks only a choice with a reason. A surface that exists only for the retired Era addon target says so plainly: it is not 1.12. - **Verification.** Every file strips to identical code, and `cargo fmt --check` is clean. Nine second readers compared every file old against new, restored the facts the rewrite had dropped, and checked each new citation against the code, the stock files, vmangos and the reference. Behaviour differences the rewrite surfaced are filed as their own issues (#131–#142), and more are being validated. The comments here state what the code does. String literals are untouched; they are #92. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
122 lines
5.1 KiB
Rust
122 lines
5.1 KiB
Rust
//! UI source files are bytes, not text. The reference reads a Lua file whole and binary
|
||
//! (`0x704bc0` via `0x648620`, no trailing NUL; binary on every leg of `0x647db0`), and
|
||
//! `0x704ae0` hands it untranscoded to `luaL_loadbuffer 0x6f5690`, so a cp1252 file's literals
|
||
//! keep their raw bytes. [`chunk`] prepares a Lua chunk byte for byte; [`decode`] gives our own
|
||
//! `.xml` and `.toc` parsers the `&str` they need. Transcoding Lua instead would change
|
||
//! `string.len`, `string.sub` and `string.byte` on every non-ASCII literal.
|
||
|
||
use std::borrow::Cow;
|
||
|
||
const BOM: &[u8] = &[0xEF, 0xBB, 0xBF];
|
||
|
||
/// `bytes` without a leading UTF-8 BOM, a strip the reference added to its
|
||
/// `luaL_loadbuffer 0x6f5690` (`0x6f5699`–`0x6f56b4`), so every chunk gets it, `loadstring`
|
||
/// included; the `.toc` reader has its own at `0x6edc71`. At most one mark, only at offset 0; a
|
||
/// UTF-16 mark is left alone, as the reference leaves it.
|
||
pub fn strip_bom(bytes: &[u8]) -> &[u8] {
|
||
bytes.strip_prefix(BOM).unwrap_or(bytes)
|
||
}
|
||
|
||
/// `bytes` without a leading `#` line: Lua 5.0 skips it for every chunk in `luaX_setinput`
|
||
/// (`0x6ff5cd`–`0x6ff600`), while 5.1 moved the skip to `luaL_loadfile`, which mlua's
|
||
/// `Lua::load` never calls. Runs after the BOM strip, the reference's order.
|
||
pub fn strip_hashbang(bytes: &[u8]) -> &[u8] {
|
||
match bytes.first() {
|
||
Some(b'#') => match bytes.iter().position(|&b| b == b'\n') {
|
||
// Lua 5.0 keeps the newline for the line counter.
|
||
Some(nl) => &bytes[nl..],
|
||
None => &bytes[bytes.len()..],
|
||
},
|
||
_ => bytes,
|
||
}
|
||
}
|
||
|
||
/// A chunk exactly as the reference's compiler receives it: [`strip_bom`] then [`strip_hashbang`].
|
||
pub fn chunk(bytes: &[u8]) -> &[u8] {
|
||
strip_hashbang(strip_bom(bytes))
|
||
}
|
||
|
||
/// UI source bytes as text for our parsers: after the BOM, valid UTF-8 is borrowed unchanged and
|
||
/// anything else is read as cp1252, which the non-UTF-8 vanilla addons (Western-European locale
|
||
/// files) are written in; every byte maps, so no file fails to decode.
|
||
pub fn decode(bytes: &[u8]) -> Cow<'_, str> {
|
||
let bytes = strip_bom(bytes);
|
||
match std::str::from_utf8(bytes) {
|
||
Ok(s) => Cow::Borrowed(s),
|
||
Err(_) => Cow::Owned(bytes.iter().map(|&b| cp1252(b)).collect()),
|
||
}
|
||
}
|
||
|
||
/// One cp1252 byte as a `char`: Latin-1, except `0x80..=0x9F`, where Latin-1 has C1 controls and
|
||
/// cp1252 has punctuation (the five undefined slots become U+FFFD).
|
||
fn cp1252(b: u8) -> char {
|
||
const C1: [char; 32] = [
|
||
'\u{20AC}', '\u{FFFD}', '\u{201A}', '\u{0192}', '\u{201E}', '\u{2026}', '\u{2020}',
|
||
'\u{2021}', '\u{02C6}', '\u{2030}', '\u{0160}', '\u{2039}', '\u{0152}', '\u{FFFD}',
|
||
'\u{017D}', '\u{FFFD}', '\u{FFFD}', '\u{2018}', '\u{2019}', '\u{201C}', '\u{201D}',
|
||
'\u{2022}', '\u{2013}', '\u{2014}', '\u{02DC}', '\u{2122}', '\u{0161}', '\u{203A}',
|
||
'\u{0153}', '\u{FFFD}', '\u{017E}', '\u{0178}',
|
||
];
|
||
match b {
|
||
0x80..=0x9F => C1[(b - 0x80) as usize],
|
||
_ => b as char,
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
#[test]
|
||
fn the_utf8_bom_is_removed_and_nothing_else_is() {
|
||
assert_eq!(strip_bom(b"\xEF\xBB\xBFlocal x = 1"), b"local x = 1");
|
||
assert_eq!(strip_bom(b"local x = 1"), b"local x = 1");
|
||
// A truncated mark is not a BOM.
|
||
assert_eq!(strip_bom(b"\xEF\xBB"), b"\xEF\xBB");
|
||
assert_eq!(strip_bom(b"\xEF"), b"\xEF");
|
||
assert_eq!(strip_bom(b""), b"");
|
||
// A UTF-16 mark is left alone.
|
||
assert_eq!(strip_bom(b"\xFF\xFEl\0"), b"\xFF\xFEl\0");
|
||
}
|
||
|
||
#[test]
|
||
fn valid_utf8_is_borrowed_unchanged() {
|
||
let text = "## Title: Sch\u{e4}tze";
|
||
assert!(matches!(decode(text.as_bytes()), Cow::Borrowed(s) if s == text));
|
||
// ...including through a BOM.
|
||
let bom = [BOM, text.as_bytes()].concat();
|
||
assert_eq!(decode(&bom), text);
|
||
}
|
||
|
||
/// `0xE4` is `ä` in Latin-1 and cp1252 alike; `0x92` is cp1252's right single quote but a C1
|
||
/// control in Latin-1.
|
||
#[test]
|
||
fn cp1252_is_decoded_where_utf8_fails() {
|
||
assert_eq!(decode(b"## Notes: Sch\xE4tze"), "## Notes: Sch\u{e4}tze");
|
||
assert_eq!(decode(b"l\x92objet"), "l\u{2019}objet");
|
||
assert_eq!(decode(b"\x80"), "\u{20AC}");
|
||
// An undefined cp1252 slot becomes the replacement char.
|
||
assert_eq!(decode(b"\x81"), "\u{FFFD}");
|
||
}
|
||
|
||
#[test]
|
||
fn a_leading_hash_line_is_eaten_the_way_lua_5_0_eats_it() {
|
||
// The newline stays, for the line counter.
|
||
assert_eq!(strip_hashbang(b"#!/usr/bin/lua\nreturn 1"), b"\nreturn 1");
|
||
assert_eq!(strip_hashbang(b"# only a comment"), b"");
|
||
assert_eq!(strip_hashbang(b"local n = #t"), b"local n = #t");
|
||
assert_eq!(strip_hashbang(b""), b"");
|
||
// The mark goes first, then the line: the reference's order.
|
||
assert_eq!(chunk(b"\xEF\xBB\xBF#line\nreturn 1"), b"\nreturn 1");
|
||
}
|
||
|
||
#[test]
|
||
fn every_byte_sequence_decodes_to_something() {
|
||
let all: Vec<u8> = (0u8..=255).collect();
|
||
let text = decode(&all);
|
||
assert!(!text.is_empty());
|
||
assert!(text.starts_with('\0'));
|
||
assert!(text.contains("ABC"));
|
||
}
|
||
}
|