Files

413 lines
18 KiB
Python
Executable File

#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0-only
"""Generate NSS-compatible UNI_*.TAB translation tables from Unicode MAPPINGS.
The generated files follow the loader shape used by Novell NSS unilib.c:
256-byte ASCII header containing "Version X.Y"
LONG tabType
if tabType == 1:
unicode_t cpTab1[256]
else:
LONG cpTab1[256]
LONG minLowerByte
LONG maxLowerByte
LONG cpTab2Count
unicode_t cpTab2[cpTab2Count]
unsigned int uniTab2Count
unsigned short uniTab1[256]
unsigned short uniTab2[uniTab2Count]
No Novell unitables data is copied. Inputs are Unicode.org mapping files.
"""
from __future__ import annotations
import argparse
import struct
from dataclasses import dataclass
from pathlib import Path
UNI_UNDEFINED = 0xFFFD
CP_UNDEFINED = 0xFDFF
SINGLE_LOOKUP_TAG = 0x0F000000
@dataclass(frozen=True)
class UniTableSpec:
filename: str
codepage: int
source: str
description: str
reversed_columns: bool = False
# NetWare/NSS-visible DOS/Windows and Macintosh tables that are present in the
# bundled Unicode.org MAPPINGS tree. UNI_000.TAB is generated separately from
# UnicodeData.txt because it has the NSS collation/case-table layout rather than
# the codepage translation-table layout used by the other .TAB files.
TABLES: tuple[UniTableSpec, ...] = (
UniTableSpec("UNI_437.TAB", 437, "VENDORS/MICSFT/PC/CP437.TXT", "DOS Latin US"),
UniTableSpec("UNI_737.TAB", 737, "VENDORS/MICSFT/PC/CP737.TXT", "DOS Greek"),
UniTableSpec("UNI_775.TAB", 775, "VENDORS/MICSFT/PC/CP775.TXT", "DOS Baltic"),
UniTableSpec("UNI_850.TAB", 850, "VENDORS/MICSFT/PC/CP850.TXT", "DOS Latin 1"),
UniTableSpec("UNI_852.TAB", 852, "VENDORS/MICSFT/PC/CP852.TXT", "DOS Latin 2"),
UniTableSpec("UNI_855.TAB", 855, "VENDORS/MICSFT/PC/CP855.TXT", "DOS Cyrillic"),
UniTableSpec("UNI_857.TAB", 857, "VENDORS/MICSFT/PC/CP857.TXT", "DOS Turkish"),
UniTableSpec("UNI_860.TAB", 860, "VENDORS/MICSFT/PC/CP860.TXT", "DOS Portuguese"),
UniTableSpec("UNI_861.TAB", 861, "VENDORS/MICSFT/PC/CP861.TXT", "DOS Icelandic"),
UniTableSpec("UNI_862.TAB", 862, "VENDORS/MICSFT/PC/CP862.TXT", "DOS Hebrew"),
UniTableSpec("UNI_863.TAB", 863, "VENDORS/MICSFT/PC/CP863.TXT", "DOS Canadian French"),
UniTableSpec("UNI_864.TAB", 864, "VENDORS/MICSFT/PC/CP864.TXT", "DOS Arabic"),
UniTableSpec("UNI_865.TAB", 865, "VENDORS/MICSFT/PC/CP865.TXT", "DOS Nordic"),
UniTableSpec("UNI_866.TAB", 866, "VENDORS/MICSFT/PC/CP866.TXT", "DOS Russian"),
UniTableSpec("UNI_869.TAB", 869, "VENDORS/MICSFT/PC/CP869.TXT", "DOS Greek 2"),
UniTableSpec("UNI_874.TAB", 874, "VENDORS/MICSFT/PC/CP874.TXT", "DOS Thai"),
UniTableSpec("UNI_932.TAB", 932, "VENDORS/MICSFT/WINDOWS/CP932.TXT", "Shift-JIS Japanese"),
UniTableSpec("UNI_936.TAB", 936, "VENDORS/MICSFT/WINDOWS/CP936.TXT", "GBK Chinese Simplified"),
UniTableSpec("UNI_949.TAB", 949, "VENDORS/MICSFT/WINDOWS/CP949.TXT", "Korean Unified Hangul"),
UniTableSpec("UNI_950.TAB", 950, "VENDORS/MICSFT/WINDOWS/CP950.TXT", "Big5 Chinese Traditional"),
UniTableSpec("UNI_1250.TAB", 1250, "VENDORS/MICSFT/WINDOWS/CP1250.TXT", "Windows Central European"),
UniTableSpec("UNI_1251.TAB", 1251, "VENDORS/MICSFT/WINDOWS/CP1251.TXT", "Windows Cyrillic"),
UniTableSpec("UNI_1252.TAB", 1252, "VENDORS/MICSFT/WINDOWS/CP1252.TXT", "Windows Latin 1"),
UniTableSpec("UNI_1253.TAB", 1253, "VENDORS/MICSFT/WINDOWS/CP1253.TXT", "Windows Greek"),
UniTableSpec("UNI_1254.TAB", 1254, "VENDORS/MICSFT/WINDOWS/CP1254.TXT", "Windows Turkish"),
UniTableSpec("UNI_1255.TAB", 1255, "VENDORS/MICSFT/WINDOWS/CP1255.TXT", "Windows Hebrew"),
UniTableSpec("UNI_1256.TAB", 1256, "VENDORS/MICSFT/WINDOWS/CP1256.TXT", "Windows Arabic"),
UniTableSpec("UNI_1257.TAB", 1257, "VENDORS/MICSFT/WINDOWS/CP1257.TXT", "Windows Baltic"),
UniTableSpec("UNI_1258.TAB", 1258, "VENDORS/MICSFT/WINDOWS/CP1258.TXT", "Windows Vietnamese"),
UniTableSpec("ROMAN.TAB", 1, "VENDORS/APPLE/ROMAN.TXT", "Macintosh Roman"),
UniTableSpec("CENTEURO.TAB", 2, "VENDORS/APPLE/CENTEURO.TXT", "Macintosh Central European"),
UniTableSpec("JAPANESE.TAB", 3, "VENDORS/APPLE/JAPANESE.TXT", "Macintosh Japanese"),
UniTableSpec("KOREAN.TAB", 4, "VENDORS/APPLE/KOREAN.TXT", "Macintosh Korean"),
UniTableSpec("CHINSIMP.TAB", 5, "VENDORS/APPLE/CHINSIMP.TXT", "Macintosh Chinese Simplified"),
UniTableSpec("CHINTRAD.TAB", 6, "VENDORS/APPLE/CHINTRAD.TXT", "Macintosh Chinese Traditional"),
UniTableSpec("ARABIC.TAB", 7, "VENDORS/APPLE/ARABIC.TXT", "Macintosh Arabic"),
UniTableSpec("CYRILLIC.TAB", 8, "VENDORS/APPLE/CYRILLIC.TXT", "Macintosh Cyrillic"),
UniTableSpec("CROATIAN.TAB", 9, "VENDORS/APPLE/CROATIAN.TXT", "Macintosh Croatian"),
UniTableSpec("DEVANAGA.TAB", 10, "VENDORS/APPLE/DEVANAGA.TXT", "Macintosh Devanagari"),
UniTableSpec("FARSI.TAB", 11, "VENDORS/APPLE/FARSI.TXT", "Macintosh Farsi"),
UniTableSpec("GREEK.TAB", 12, "VENDORS/APPLE/GREEK.TXT", "Macintosh Greek"),
UniTableSpec("GUJARATI.TAB", 13, "VENDORS/APPLE/GUJARATI.TXT", "Macintosh Gujarati"),
UniTableSpec("GURMUKHI.TAB", 14, "VENDORS/APPLE/GURMUKHI.TXT", "Macintosh Gurmukhi"),
UniTableSpec("HEBREW.TAB", 15, "VENDORS/APPLE/HEBREW.TXT", "Macintosh Hebrew"),
UniTableSpec("ICELAND.TAB", 16, "VENDORS/APPLE/ICELAND.TXT", "Macintosh Icelandic"),
UniTableSpec("KEYBOARD.TAB", 17, "VENDORS/APPLE/KEYBOARD.TXT", "Macintosh Keyboard Characters"),
UniTableSpec("ROMANIAN.TAB", 18, "VENDORS/APPLE/ROMANIAN.TXT", "Macintosh Romanian"),
UniTableSpec("SYMBOL.TAB", 19, "VENDORS/APPLE/SYMBOL.TXT", "Macintosh Symbol"),
UniTableSpec("THAI.TAB", 20, "VENDORS/APPLE/THAI.TXT", "Macintosh Thai"),
UniTableSpec("TURKISH.TAB", 21, "VENDORS/APPLE/TURKISH.TXT", "Macintosh Turkish"),
UniTableSpec("DINGBATS.TAB", 22, "VENDORS/APPLE/DINGBATS.TXT", "Macintosh Dingbats"),
UniTableSpec("CELTIC.TAB", 23, "VENDORS/APPLE/CELTIC.TXT", "Macintosh Celtic"),
UniTableSpec("GAELIC.TAB", 24, "VENDORS/APPLE/GAELIC.TXT", "Macintosh Gaelic"),
UniTableSpec("INUIT.TAB", 25, "VENDORS/APPLE/INUIT.TXT", "Macintosh Inuit"),
# Apple retired the separate Ukrainian map and points consumers at the
# Cyrillic table. Keep the NSS-visible UKRAINE.TAB name as a generated
# alias so libnwnss can resolve that historical Macintosh table request
# without copying Novell unitables.
UniTableSpec("UKRAINE.TAB", 8, "VENDORS/APPLE/CYRILLIC.TXT", "Macintosh Ukrainian alias to Cyrillic"),
UniTableSpec("ATARIST.TAB", 10000, "VENDORS/MISC/ATARIST.TXT", "Atari ST"),
UniTableSpec("UNI_1006.TAB", 1006, "VENDORS/MISC/CP1006.TXT", "IBM CP1006 Urdu"),
UniTableSpec("UNI_424.TAB", 424, "VENDORS/MISC/CP424.TXT", "IBM CP424 Hebrew EBCDIC"),
UniTableSpec("UNI_856.TAB", 856, "VENDORS/MISC/CP856.TXT", "IBM CP856 Hebrew PC"),
UniTableSpec("KOI8-R.TAB", 20866, "VENDORS/MISC/KOI8-R.TXT", "KOI8-R Cyrillic"),
UniTableSpec("KOI8-U.TAB", 21866, "VENDORS/MISC/KOI8-U.TXT", "KOI8-U Cyrillic/Ukrainian"),
UniTableSpec("KPS9566.TAB", 9566, "VENDORS/MISC/KPS9566.TXT", "KPS 9566 Korean"),
UniTableSpec("KZ1048.TAB", 1048, "VENDORS/MISC/KZ1048.TXT", "Kazakhstan KZ-1048"),
UniTableSpec("IBMGRAPH.TAB", 10001, "VENDORS/MISC/IBMGRAPH.TXT", "IBM graphic characters", True),
)
def parse_scalar_token(text: str) -> int | None:
# Apple Arabic/Farsi mappings mark display direction as <LR>+0xNNNN or
# <RL>+0xNNNN. NSS unitables cannot encode the directional override, but
# they can still carry the underlying scalar mapping. Keep true composite
# mappings skipped.
if text.startswith("<") and ">+" in text:
text = text.split(">+", 1)[1]
if "+" in text:
return None
try:
return int(text, 16)
except ValueError:
return None
def parse_mapping(path: Path, *, reversed_columns: bool = False) -> dict[int, int]:
pairs: dict[int, int] = {}
for line in path.read_text(encoding="utf-8", errors="replace").splitlines():
data = line.split("#", 1)[0].strip()
if not data:
continue
fields = data.split()
if len(fields) < 2:
continue
source_text, unicode_text = fields[0], fields[1]
if reversed_columns:
source_text, unicode_text = unicode_text, source_text
source = parse_scalar_token(source_text)
unicode = parse_scalar_token(unicode_text)
if source is None or unicode is None:
continue
if source > 0xFFFF or unicode > 0xFFFF:
continue
pairs.setdefault(source, unicode)
return pairs
def pack_i32(value: int) -> bytes:
return struct.pack("<i", value)
def pack_u32(value: int) -> bytes:
return struct.pack("<I", value)
def pack_u16(value: int) -> bytes:
return struct.pack("<H", value & 0xFFFF)
def make_header(spec: UniTableSpec) -> bytes:
text = (
f"MARS-NWE NSS Unicode/Codepage {spec.codepage} Translation Table, "
"Version 1.01\n"
f"Generated from Unicode.org Public/MAPPINGS/{spec.source}\n"
"No Novell unitables data copied.\n"
).encode("ascii", errors="replace")
if len(text) > 256:
raise ValueError(f"header too long for {spec.filename}")
return text + (b" " * (256 - len(text)))
def emit_mb_to_unicode(mapping: dict[int, int]) -> bytes:
max_source = max(mapping.keys(), default=0)
out = bytearray()
if max_source <= 0xFF:
out += pack_i32(1)
table = [UNI_UNDEFINED] * 256
for source, unicode in mapping.items():
table[source] = unicode
for unicode in table:
out += pack_u16(unicode)
return bytes(out)
out += pack_i32(2)
double_by_lead: dict[int, dict[int, int]] = {}
single_by_lead: dict[int, int] = {}
lows: list[int] = []
for source, unicode in mapping.items():
if source <= 0xFF:
single_by_lead[source] = unicode
continue
lead = (source >> 8) & 0xFF
low = source & 0xFF
double_by_lead.setdefault(lead, {})[low] = unicode
lows.append(low)
min_low = min(lows) if lows else 0
max_low = max(lows) if lows else 0
width = max_low - min_low + 1 if lows else 0
cp_tab1 = [SINGLE_LOOKUP_TAG | UNI_UNDEFINED] * 256
cp_tab2: list[int] = []
for lead in range(256):
if lead in double_by_lead:
# NSS unilib.c indexes type-2 second-stage tables as
# cpTab2[cpTab1[lead] + lowByte], after separately checking
# minLowerByte <= lowByte <= maxLowerByte. Store the base
# offset already biased by -minLowerByte so the loader lands at
# the first entry in this lead-byte chunk for low == minLowerByte.
cp_tab1[lead] = len(cp_tab2) - min_low
chunk = [UNI_UNDEFINED] * width
for low, unicode in double_by_lead[lead].items():
chunk[low - min_low] = unicode
cp_tab2.extend(chunk)
elif lead in single_by_lead:
cp_tab1[lead] = SINGLE_LOOKUP_TAG | single_by_lead[lead]
for value in cp_tab1:
out += pack_i32(value)
out += pack_i32(min_low)
out += pack_i32(max_low)
out += pack_i32(len(cp_tab2))
for unicode in cp_tab2:
out += pack_u16(unicode)
return bytes(out)
def encode_output_word(source: int) -> int:
if source <= 0xFF:
return source
# NSS uni2loc() writes FIRST_BYTE(copy) and then SECOND_BYTE(copy), where
# copy.word is read through a little-endian byte pair. Store multibyte
# source values byte-swapped so a logical 0x8140 mapping emits bytes
# 0x81, 0x40 instead of 0x40, 0x81.
return ((source & 0x00FF) << 8) | ((source >> 8) & 0x00FF)
def emit_unicode_to_mb(mapping: dict[int, int]) -> bytes:
# Prefer the first/lowest encoded representation for duplicate Unicode
# points. NSS stores the encoded result in a 16-bit word, with byte[0]
# consumed first on little-endian hosts.
unicode_to_code: dict[int, int] = {}
for source, unicode in sorted(mapping.items()):
if unicode == 0 or unicode > 0xFFFF:
continue
unicode_to_code.setdefault(unicode, encode_output_word(source))
default_page = [CP_UNDEFINED] * 256
pages: dict[int, list[int]] = {}
for unicode, source in unicode_to_code.items():
high = unicode >> 8
low = unicode & 0xFF
pages.setdefault(high, default_page.copy())[low] = source
uni_tab2: list[int] = default_page.copy()
uni_tab1 = [0] * 256
for high in range(256):
page = pages.get(high)
if page is None:
uni_tab1[high] = 0
continue
uni_tab1[high] = len(uni_tab2)
uni_tab2.extend(page)
out = bytearray()
out += pack_u32(len(uni_tab2))
for offset in uni_tab1:
out += pack_u16(offset)
for code in uni_tab2:
out += pack_u16(code)
return bytes(out)
def emit_table(spec: UniTableSpec, mappings_root: Path, output_dir: Path) -> tuple[int, int]:
source_path = mappings_root / spec.source
if not source_path.exists():
raise FileNotFoundError(source_path)
mapping = parse_mapping(source_path, reversed_columns=spec.reversed_columns)
if not mapping:
raise ValueError(f"no direct mappings found in {source_path}")
data = make_header(spec) + emit_mb_to_unicode(mapping) + emit_unicode_to_mb(mapping)
output_path = output_dir / spec.filename
output_path.write_bytes(data)
return len(mapping), len(data)
MAPPING_TO_UPPER_CASE_BIT = 0x08000000
MAPPING_TO_LOWER_CASE_BIT = 0x04000000
OFFSET_MASK = 0x000FFFFF
UNI_000_MASTER_COUNT = 0x10000
UNI_000_DATA_COUNT = 46507
def parse_unicode_case_data(ucd_dir: Path) -> tuple[list[int], list[int], dict[str, int]]:
unicode_data = ucd_dir / "UnicodeData.txt"
if not unicode_data.exists():
raise FileNotFoundError(unicode_data)
lower = list(range(UNI_000_MASTER_COUNT))
upper = list(range(UNI_000_MASTER_COUNT))
stats = {
"records": 0,
"lower_mappings": 0,
"upper_mappings": 0,
"non_bmp_skipped": 0,
}
with unicode_data.open("r", encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line or line.startswith("#"):
continue
fields = line.split(";")
if len(fields) < 15:
raise ValueError(f"Malformed UnicodeData line: {line!r}")
cp = int(fields[0], 16)
stats["records"] += 1
if cp >= UNI_000_MASTER_COUNT:
stats["non_bmp_skipped"] += 1
continue
upper_field = fields[12]
lower_field = fields[13]
if upper_field:
target = int(upper_field, 16)
if target < UNI_000_MASTER_COUNT:
upper[cp] = target
stats["upper_mappings"] += 1
if lower_field:
target = int(lower_field, 16)
if target < UNI_000_MASTER_COUNT:
lower[cp] = target
stats["lower_mappings"] += 1
return lower, upper, stats
def emit_uni_000(ucd_dir: Path, output_dir: Path) -> tuple[int, int, int]:
lower, upper, stats = parse_unicode_case_data(ucd_dir)
master = [0] * UNI_000_MASTER_COUNT
data: list[int] = []
for cp in range(UNI_000_MASTER_COUNT):
flags = 0
offset = len(data)
if lower[cp] != cp:
flags |= MAPPING_TO_LOWER_CASE_BIT
data.append(lower[cp])
if upper[cp] != cp:
flags |= MAPPING_TO_UPPER_CASE_BIT
data.append(upper[cp])
if flags:
if offset > OFFSET_MASK:
raise ValueError("UNI_000.TAB case table offset exceeds NSS OFFSET_MASK")
master[cp] = flags | offset
used_data = len(data)
if used_data > UNI_000_DATA_COUNT:
raise ValueError(f"UNI_000.TAB case table needs {used_data} LONGs, NSS loader reads {UNI_000_DATA_COUNT}")
data.extend([0] * (UNI_000_DATA_COUNT - used_data))
output_path = output_dir / "UNI_000.TAB"
with output_path.open("wb") as out:
for value in master:
out.write(pack_u32(value))
for value in data:
out.write(pack_u32(value))
return used_data, output_path.stat().st_size, stats["records"]
def write_summary(output_dir: Path, rows: list[tuple[UniTableSpec, int, int]], uni000: tuple[int, int, int]) -> None:
uni000_used, uni000_size, unicode_records = uni000
with (output_dir / "README.md").open("w", encoding="utf-8") as out:
out.write("# Generated NSS-compatible unitables\n\n")
out.write("Generated by `scripts/gen_nss_unitables.py` from Unicode.org `MAPPINGS/` and `UCD/UnicodeData.txt`.\n\n")
out.write("These binary `.TAB` files follow the table layouts consumed by the NSS `unilib.c` loader. ")
out.write("They intentionally do not copy Novell `shared/sdk/unitables/*.TAB` data.\n\n")
out.write("`UNI_000.TAB` is the NSS resident collation/case table file. The generated file currently ")
out.write("populates the loader-visible simple BMP lower/upper-case mappings from UnicodeData.txt and ")
out.write("pads the original 46507-LONG data-table area expected by the loader.\n\n")
out.write("| File | Codepage | Source | Pairs/Data LONGs | Bytes |\n")
out.write("| --- | ---: | --- | ---: | ---: |\n")
out.write(f"| `UNI_000.TAB` | 0 | `UCD/UnicodeData.txt` ({unicode_records} records) | {uni000_used} | {uni000_size} |\n")
for spec, pairs, size in rows:
out.write(f"| `{spec.filename}` | {spec.codepage} | `{spec.source}` | {pairs} | {size} |\n")
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--mappings-root", type=Path, default=Path("MAPPINGS"))
parser.add_argument("--ucd-dir", type=Path, default=Path("UCD"))
parser.add_argument("--output-dir", type=Path, default=Path("TAB/unitables"))
args = parser.parse_args()
output_dir = args.output_dir
output_dir.mkdir(parents=True, exist_ok=True)
uni000 = emit_uni_000(args.ucd_dir, output_dir)
rows = []
for spec in TABLES:
pairs, size = emit_table(spec, args.mappings_root, output_dir)
rows.append((spec, pairs, size))
write_summary(output_dir, rows, uni000)
if __name__ == "__main__":
main()