001package gudusoft.gsqlparser.sqlenv.unicode;
002
003import java.io.DataInputStream;
004import java.io.IOException;
005import java.io.InputStream;
006
007/**
008 * GSP-pinned Unicode case operations — the JVM-independent fold engine of the
009 * persistent canonical-identity program
010 * (docs/designs/persistent-canonical-identity-implementation-plan.md §3).
011 *
012 * <p>All data comes from the committed binary resource {@code gsp-case-data.bin},
013 * generated from vendored UCD {@value #UNICODE_VERSION} files. Nothing here consults
014 * {@code String.toUpperCase}/{@code toLowerCase} or any other JVM Unicode table, so the
015 * results are identical on every JVM — the whole point: JDK 8 ships Unicode 6.2 while
016 * JDK 21 ships 15.x, and identifier identity must not depend on which one happens to
017 * run the export.
018 *
019 * <p>The operations, deliberately nothing else:
020 * <ul>
021 *   <li>{@link #toUpperFull}/{@link #toLowerFull} — FULL case mappings (length may
022 *       change: {@code ß→SS}), plus Final_Sigma as the single context rule, implemented
023 *       per the UCD definition (NOT any JDK's quirk — the JDKs disagree with each other
024 *       on Case_Ignorable contexts, so "match the JVM" is not even well-defined).
025 *       Locale-conditional rules (tr/az/lt) are excluded by design: no database folds
026 *       identifiers by session locale, and including them corrupts catalog identity.</li>
027 *   <li>{@link #simpleFoldCollapse} — CaseFolding.txt C+S simple fold, code-point-wise.
028 *       Successor of the char-wise {@code caseCollapse} (fixes supplementary planes).</li>
029 *   <li>{@link #toUpperSimple}/{@link #toLowerSimple} — SIMPLE case mappings
030 *       (UnicodeData.txt fields 12/13), one code point to one, no context: the mappings
031 *       Oracle (unquoted names) and MySQL (table, column and routine names) actually apply.
032 *       Derived from the same resource plus a small checked override table, so they add no
033 *       resource and change none of the operations above.</li>
034 *   <li>{@link #toLowerAscii} and {@link #clipUtf8Bytes} — PostgreSQL's identifier
035 *       downcasing in a UTF8 database and its NAMEDATALEN truncation; no Unicode data.</li>
036 * </ul>
037 *
038 * <p>Unpaired surrogates pass through unchanged in every operation — identity must be
039 * total on arbitrary UTF-16, and substitution would merge distinct malformed inputs.
040 * Inputs the operation does not change are returned as the SAME instance, including
041 * surrogate-bearing ones (zero allocation).
042 *
043 * <p>Thread-safe: one immutable table holder, safely published via the class
044 * initializer, whose body is straight-line code delegating to ordinary methods.
045 * (Historical note: this class's first obfuscated build failed verification because
046 * proguard.cfg once disabled preverification while emitting v52 class files — that
047 * affected EVERY branchy method product-wide and was fixed by re-enabling
048 * preverification; the branch-free initializer is retained as cheap hygiene.)
049 * Deployment notes: the resource is looked up relative to this class, so package
050 * RELOCATION (consumer-side shading) relocates the lookup with it, but the relocator
051 * must also move the resource directory; GraalVM native-image users must register
052 * {@code gudusoft/gsqlparser/sqlenv/unicode/gsp-case-data.bin} as a resource.
053 * Missing/corrupt resource fails fast with {@link ExceptionInInitializerError} on
054 * first use.
055 */
056public final class GspCaseData {
057
058    /** Exact UCD version the tables are generated from. Part of every policyId. */
059    public static final String UNICODE_VERSION = "16.0.0";
060
061    /** Bumped on ANY behavioral change of these operations. Part of every policyId. */
062    public static final int ALGORITHM_REV = 1;
063
064    private static final int SIGMA = 0x03A3;        // Σ
065    private static final int FINAL_SIGMA = 0x03C2;  // ς
066
067    // value 0 = no mapping (identity); MULTI = consult the sorted arrays.
068    // U+FFFF is a noncharacter and never a mapping target (asserted by the
069    // conformance suite against the audit dump), so it is a safe sentinel.
070    private static final char MULTI = (char) 0xFFFF;
071
072    /** Immutable table holder — loaded by ordinary (verifiable) methods; the
073     *  initializer below is pure straight-line code (no branches, no frames), and the
074     *  static-final aliases restore JIT constant folding lost to instance-field
075     *  indirection. */
076    private static final Data D = Data.load();
077    private static final int[] UPPER_KEYS = D.upperKeys;
078    private static final String[] UPPER_VALUES = D.upperValues;
079    private static final int[] LOWER_KEYS = D.lowerKeys;
080    private static final String[] LOWER_VALUES = D.lowerValues;
081    private static final int[] FOLD_KEYS = D.foldKeys;
082    private static final String[] FOLD_VALUE_STRINGS = D.foldValueStrings;
083    private static final int[] UPPER_BLOCKS = D.upperBlocks;
084    private static final int[] LOWER_BLOCKS = D.lowerBlocks;
085    private static final int[] FOLD_BLOCKS = D.foldBlocks;
086
087    /**
088     * Reusable mapping buffer for the 1:1 fast path (U5 fork-pair benchmark:
089     * the per-call {@code char[n]} plus {@code String}'s internal copy tripled
090     * allocation vs the JDK's in-{@code java.lang} zero-copy path). The buffer
091     * never escapes — {@code new String(buf, 0, n)} copies — and {@code map}
092     * neither recurses nor calls back into itself, so per-thread reuse is
093     * safe. Inputs longer than the buffer allocate as before.
094     */
095    private static final int SCRATCH_LIMIT = 256;
096    private static final ThreadLocal<char[]> SCRATCH = new ThreadLocal<char[]>() {
097        @Override
098        protected char[] initialValue() {
099            return new char[SCRATCH_LIMIT];
100        }
101    };
102    private static final char[] UPPER_BMP = D.upperBmp;
103    private static final char[] LOWER_BMP = D.lowerBmp;
104    private static final char[] FOLD_BMP = D.foldBmp;
105    private static final char[] UPPER_SIMPLE_BMP = D.upperSimpleBmp;
106    private static final char[] LOWER_SIMPLE_BMP = D.lowerSimpleBmp;
107    private static final int[] CASED_STARTS = D.casedStarts;
108    private static final int[] CASED_ENDS = D.casedEnds;
109    private static final int[] IGNORABLE_STARTS = D.ignorableStarts;
110    private static final int[] IGNORABLE_ENDS = D.ignorableEnds;
111
112    private GspCaseData() {
113    }
114
115    /** Full uppercase mapping (unconditional UCD data; length may grow, e.g. ß→SS). */
116    public static String toUpperFull(String s) {
117        return map(s, UPPER_BMP, UPPER_KEYS, UPPER_BLOCKS, UPPER_VALUES, false);
118    }
119
120    /**
121     * Full lowercase mapping with the UCD Final_Sigma rule: Σ lowers to ς iff preceded
122     * by a Cased code point (skipping Case_Ignorable) and NOT followed by one (skipping
123     * Case_Ignorable). Everything else uses unconditional data.
124     */
125    public static String toLowerFull(String s) {
126        return map(s, LOWER_BMP, LOWER_KEYS, LOWER_BLOCKS, LOWER_VALUES, true);
127    }
128
129    /**
130     * CaseFolding C+S simple fold applied per code point — the canonical-equality
131     * collapse for INSENSITIVE + no-fold cells. Code-point-wise, so supplementary-plane
132     * pairs fold correctly (unlike the char-wise legacy collapse).
133     */
134    public static String simpleFoldCollapse(String s) {
135        return map(s, FOLD_BMP, FOLD_KEYS, FOLD_BLOCKS, FOLD_VALUE_STRINGS, false);
136    }
137
138    /**
139     * Simple uppercase mapping, code point by code point: each code point becomes its
140     * Simple_Uppercase_Mapping (UnicodeData.txt field 12) or stays itself. One code point in,
141     * one code point out, no context, no locale ({@code ß} is kept, unlike {@link #toUpperFull}).
142     *
143     * <p>This is the per-character mapping Oracle applies to an unquoted name
144     * ({@code straße} is stored as {@code STRAßE}) and MySQL applies when it compares routine
145     * names ({@code ı} and {@code i} are one function). It is the SQL-LR {@code UPPER_SIMPLE}
146     * operation (lineage-spec/identifier-rules/FORMAT.md section 6).
147     */
148    public static String toUpperSimple(String s) {
149        return map(s, UPPER_SIMPLE_BMP, UPPER_KEYS, UPPER_BLOCKS, UPPER_VALUES, false);
150    }
151
152    /**
153     * Simple lowercase mapping, code point by code point: each code point becomes its
154     * Simple_Lowercase_Mapping (UnicodeData.txt field 13) or stays itself. No Final_Sigma
155     * context ({@code Σ} always becomes {@code σ}), and {@code İ} becomes {@code i}.
156     *
157     * <p>This is MySQL's one-to-one lowercase mapping for table names under
158     * {@code lower_case_table_names} 1 and 2 and for column names. It is the SQL-LR
159     * {@code LOWER_SIMPLE} operation.
160     */
161    public static String toLowerSimple(String s) {
162        return map(s, LOWER_SIMPLE_BMP, LOWER_KEYS, LOWER_BLOCKS, LOWER_VALUES, false);
163    }
164
165    /**
166     * ASCII-only lowercase: {@code A}..{@code Z} become {@code a}..{@code z}, every other code
167     * unit is kept. PostgreSQL's {@code downcase_identifier} in a multibyte server encoding
168     * (UTF8): {@code ÄBC} is stored as {@code Äbc}. Consults no Unicode data. Returns the same
169     * instance when nothing changes.
170     */
171    public static String toLowerAscii(String s) {
172        if (s == null) {
173            return null;
174        }
175        int n = s.length();
176        int i = 0;
177        while (i < n) {
178            char c = s.charAt(i);
179            if (c >= 'A' && c <= 'Z') {
180                break;
181            }
182            i++;
183        }
184        if (i == n) {
185            return s;
186        }
187        char[] out = s.toCharArray();
188        for (; i < n; i++) {
189            char c = out[i];
190            if (c >= 'A' && c <= 'Z') {
191                out[i] = (char) (c + ('a' - 'A'));
192            }
193        }
194        return new String(out);
195    }
196
197    /**
198     * The longest prefix of {@code s} whose UTF-8 encoding has at most {@code maxBytes} bytes,
199     * cut on a whole code point (PostgreSQL's {@code pg_mbcliplen} in a UTF8 database). Returns
200     * the same instance when it already fits. An unpaired surrogate counts as three bytes (its
201     * CESU form): it never occurs in a name a server stored, and counting it keeps the
202     * operation total on arbitrary UTF-16.
203     *
204     * @param s the text; null returns null
205     * @param maxBytes the byte limit; zero or negative means no limit
206     */
207    public static String clipUtf8Bytes(String s, int maxBytes) {
208        if (s == null || maxBytes <= 0) {
209            return s;
210        }
211        int n = s.length();
212        // Every UTF-16 unit encodes to at most 3 UTF-8 bytes (a surrogate pair, 2 units, to 4).
213        if ((long) n * 3 <= maxBytes) {
214            return s;
215        }
216        int bytes = 0;
217        int i = 0;
218        while (i < n) {
219            char c = s.charAt(i);
220            int units = 1;
221            int width;
222            if (c < 0x80) {
223                width = 1;
224            } else if (c < 0x800) {
225                width = 2;
226            } else if (Character.isHighSurrogate(c) && i + 1 < n
227                    && Character.isLowSurrogate(s.charAt(i + 1))) {
228                width = 4;
229                units = 2;
230            } else {
231                width = 3;
232            }
233            if (bytes + width > maxBytes) {
234                return s.substring(0, i);
235            }
236            bytes += width;
237            i += units;
238        }
239        return s;
240    }
241
242    // ---- the mapper: ASCII tight path, then general code-point path ----
243
244    /**
245     * One mapper for all three operations; {@code finalSigma} enables the Σ context
246     * rule (lowercasing only). Two-tier shape (U5 gate 3): fixed points (nothing
247     * maps — the dominant identifier case) return the SAME instance with zero
248     * allocation, including surrogate-bearing inputs (valid pairs are probed
249     * against the supplementary keys; unpaired units are passthrough by policy).
250     * Strings whose mappings are all 1:1 in UTF-16 units — BMP single-char
251     * mappings, supplementary pairs mapping to one code point, and Σ (σ and ς are
252     * both one unit; the context scan reads the ORIGINAL string, so mapped output
253     * never affects it) — are built in a fixed {@code char[n]}. Only a length-
254     * changing mapping (MULTI expansions like ß→SS) drops to the StringBuilder
255     * walk, resuming exactly where the fast loop stopped.
256     */
257    private static String map(String s, char[] bmp, int[] keys, int[] blocks,
258                              String[] values, boolean finalSigma) {
259        if (s == null || s.isEmpty()) {
260            return s;
261        }
262        int n = s.length();
263        int first = firstMappedIndex(s, 0, bmp, keys, blocks);
264        if (first == n) {
265            return s; // fixed point: same instance, zero allocation
266        }
267        char[] out = n <= SCRATCH_LIMIT ? SCRATCH.get() : new char[n];
268        s.getChars(0, first, out, 0);
269        int j = first;
270        while (j < n) {
271            char d = s.charAt(j);
272            if (d < Character.MIN_SURROGATE || d > Character.MAX_SURROGATE) {
273                if (finalSigma && d == (char) SIGMA && isFinalSigmaContext(s, j)) {
274                    out[j] = (char) FINAL_SIGMA;
275                    j++;
276                    continue;
277                }
278                char md = bmp[d];
279                if (md == MULTI) {
280                    break; // length-changing expansion: finish on the slow path
281                }
282                out[j] = md == 0 ? d : md;
283                j++;
284                continue;
285            }
286            int cp = codePointOrUnit(s, j);
287            if (cp <= 0xFFFF) {
288                out[j] = d; // unpaired surrogate passthrough
289                j++;
290                continue;
291            }
292            int idx = lookup(keys, blocks, cp);
293            if (idx < 0) {
294                out[j] = d;
295                out[j + 1] = s.charAt(j + 1);
296                j += 2;
297                continue;
298            }
299            String v = values[idx];
300            if (v.length() != 2) {
301                break; // supplementary mapping that changes unit count: slow path
302            }
303            out[j] = v.charAt(0);
304            out[j + 1] = v.charAt(1);
305            j += 2;
306        }
307        if (j == n) {
308            return new String(out, 0, n);
309        }
310        StringBuilder b = new StringBuilder(n + 4);
311        b.append(out, 0, j);
312        return mapTail(s, j, b, bmp, keys, blocks, values, finalSigma);
313    }
314
315    /**
316     * Code-point lookup in a sorted keys table via its per-256-codepoint block
317     * index: jump straight to the (contiguous, cache-local) block slice and
318     * binary-search only inside it — replaces full-table binary search, whose
319     * ~11 scattered probes dominated supplementary-plane rows in the U5
320     * fork-pair benchmark (the "sparse per-plane lookup" review round 2
321     * suggested).
322     */
323    private static int lookup(int[] keys, int[] blocks, int cp) {
324        int b = cp >>> 8;
325        return java.util.Arrays.binarySearch(keys, blocks[b], blocks[b + 1], cp);
326    }
327
328    /** General walk from {@code i} appending to {@code b} (the pre-{@code i} text is
329     *  already appended). Handles MULTI expansions, supplementary keys, unpaired
330     *  surrogates, and — when {@code finalSigma} — the Σ context rule. */
331    private static String mapTail(String s, int i, StringBuilder b,
332                                  char[] bmp, int[] keys, int[] blocks,
333                                  String[] values, boolean finalSigma) {
334        int n = s.length();
335        while (i < n) {
336            char c = s.charAt(i);
337            if (c < Character.MIN_SURROGATE || c > Character.MAX_SURROGATE) {
338                if (finalSigma && c == (char) SIGMA && isFinalSigmaContext(s, i)) {
339                    b.append((char) FINAL_SIGMA);
340                    i++;
341                    continue;
342                }
343                char m = bmp[c];
344                if (m == 0) {
345                    b.append(c);
346                } else if (m != MULTI) {
347                    b.append(m);
348                } else {
349                    b.append(values[lookup(keys, blocks, c)]);
350                }
351                i++;
352                continue;
353            }
354            int cp = codePointOrUnit(s, i);
355            if (cp > 0xFFFF) {
356                int idx = lookup(keys, blocks, cp);
357                if (idx >= 0) {
358                    b.append(values[idx]);
359                } else {
360                    b.appendCodePoint(cp);
361                }
362                i += 2;
363            } else {
364                b.append((char) cp); // unpaired surrogate passthrough
365                i++;
366            }
367        }
368        return b.toString();
369    }
370
371    /**
372     * Index of the first UTF-16 position at or after {@code from} whose content the
373     * operation would map, or {@code s.length()} when the rest is a fixed point.
374     * Σ needs no special case: {@code LOWER_BMP[Σ]} is non-zero (σ), so a Σ always
375     * reports as mapped and the tail walk decides between σ and ς.
376     */
377    private static int firstMappedIndex(String s, int from, char[] bmp, int[] keys,
378                                        int[] blocks) {
379        int n = s.length();
380        int i = from;
381        while (i < n) {
382            char c = s.charAt(i);
383            if (c < Character.MIN_SURROGATE || c > Character.MAX_SURROGATE) {
384                if (bmp[c] != 0) {
385                    return i;
386                }
387                i++;
388                continue;
389            }
390            int cp = codePointOrUnit(s, i);
391            if (cp > 0xFFFF) {
392                if (lookup(keys, blocks, cp) >= 0) {
393                    return i;
394                }
395                i += 2;
396            } else {
397                i++; // unpaired surrogate: passthrough, keep scanning
398            }
399        }
400        return n;
401    }
402
403    // ---- Final_Sigma context (UCD definition, Cased/Case_Ignorable from DCP) ----
404
405    private static boolean isFinalSigmaContext(String s, int sigmaIndex) {
406        // Before: scanning backwards, skip Case_Ignorable; require Cased. Note the
407        // sets OVERLAP (U+0345 is both) — per the spec's possessive skipping, a
408        // code point that is Case_Ignorable is skipped regardless of Cased-ness.
409        int i = sigmaIndex;
410        boolean casedBefore = false;
411        while (i > 0) {
412            int cp = codePointBefore(s, i);
413            i -= cp > 0xFFFF ? 2 : 1;
414            if (isCaseIgnorable(cp)) {
415                continue;
416            }
417            casedBefore = isCased(cp);
418            break;
419        }
420        if (!casedBefore) {
421            return false;
422        }
423        // After: scanning forwards, skip Case_Ignorable; require NOT Cased.
424        i = sigmaIndex + 1; // Σ is a BMP char
425        while (i < s.length()) {
426            int cp = codePointOrUnit(s, i);
427            i += cp > 0xFFFF ? 2 : 1;
428            if (isCaseIgnorable(cp)) {
429                continue;
430            }
431            return !isCased(cp);
432        }
433        return true; // end of string after skipping ignorables
434    }
435
436    static boolean isCased(int cp) {
437        return inRanges(cp, CASED_STARTS, CASED_ENDS);
438    }
439
440    static boolean isCaseIgnorable(int cp) {
441        return inRanges(cp, IGNORABLE_STARTS, IGNORABLE_ENDS);
442    }
443
444    private static boolean inRanges(int cp, int[] starts, int[] ends) {
445        int idx = java.util.Arrays.binarySearch(starts, cp);
446        if (idx >= 0) {
447            return true;
448        }
449        int insertion = -idx - 2; // candidate range that starts before cp
450        return insertion >= 0 && cp <= ends[insertion];
451    }
452
453    // ---- UTF-16 walking that is total on malformed input ----
454
455    /** Code point at i, or the lone surrogate unit itself when unpaired. */
456    private static int codePointOrUnit(String s, int i) {
457        char c = s.charAt(i);
458        if (Character.isHighSurrogate(c) && i + 1 < s.length()
459                && Character.isLowSurrogate(s.charAt(i + 1))) {
460            return Character.toCodePoint(c, s.charAt(i + 1));
461        }
462        return c; // BMP char, or unpaired surrogate passed through as its unit value
463    }
464
465    private static int codePointBefore(String s, int i) {
466        char c = s.charAt(i - 1);
467        if (Character.isLowSurrogate(c) && i - 2 >= 0
468                && Character.isHighSurrogate(s.charAt(i - 2))) {
469            return Character.toCodePoint(s.charAt(i - 2), c);
470        }
471        return c;
472    }
473
474    // ---- the table holder: loaded by ordinary (verifiable) methods ----
475
476    private static final class Data {
477        final int[] upperKeys;
478        final String[] upperValues;
479        final int[] lowerKeys;
480        final String[] lowerValues;
481        final int[] foldKeys;
482        final String[] foldValueStrings;
483        final char[] upperBmp = new char[0x10000];
484        final char[] lowerBmp = new char[0x10000];
485        final char[] foldBmp = new char[0x10000];
486        final char[] upperSimpleBmp;
487        final char[] lowerSimpleBmp;
488        final int[] casedStarts;
489        final int[] casedEnds;
490        final int[] ignorableStarts;
491        final int[] ignorableEnds;
492        final int[] upperBlocks;
493        final int[] lowerBlocks;
494        final int[] foldBlocks;
495
496        private Data(int[] upperKeys, String[] upperValues,
497                     int[] lowerKeys, String[] lowerValues,
498                     int[] foldKeys, int[] foldValues,
499                     int[] casedStarts, int[] casedEnds,
500                     int[] ignorableStarts, int[] ignorableEnds) {
501            this.upperKeys = upperKeys;
502            this.upperValues = upperValues;
503            this.lowerKeys = lowerKeys;
504            this.lowerValues = lowerValues;
505            this.foldKeys = foldKeys;
506            this.casedStarts = casedStarts;
507            this.casedEnds = casedEnds;
508            this.ignorableStarts = ignorableStarts;
509            this.ignorableEnds = ignorableEnds;
510            this.foldValueStrings = new String[foldValues.length];
511            for (int i = 0; i < foldValues.length; i++) {
512                StringBuilder fv = new StringBuilder(2);
513                fv.appendCodePoint(foldValues[i]);
514                this.foldValueStrings[i] = fv.toString();
515            }
516            deriveBmp(upperKeys, upperValues, upperBmp);
517            deriveBmp(lowerKeys, lowerValues, lowerBmp);
518            for (int i = 0; i < foldKeys.length; i++) {
519                if (foldKeys[i] <= 0xFFFF) {
520                    foldBmp[foldKeys[i]] = foldValues[i] <= 0xFFFF
521                            ? (char) foldValues[i] : MULTI;
522                }
523            }
524            this.upperBlocks = blockIndex(upperKeys);
525            this.lowerBlocks = blockIndex(lowerKeys);
526            this.foldBlocks = blockIndex(foldKeys);
527            this.upperSimpleBmp = deriveSimpleBmp(upperBmp, SIMPLE_UPPER_OVERRIDES, "upper");
528            this.lowerSimpleBmp = deriveSimpleBmp(lowerBmp, SIMPLE_LOWER_OVERRIDES, "lower");
529        }
530
531        /**
532         * The simple (one code point to one code point) mapping differs from the full mapping
533         * of the binary resource exactly where SpecialCasing.txt has an unconditional entry; in
534         * UCD {@value GspCaseData#UNICODE_VERSION} every such entry is a length-changing ({@link #MULTI})
535         * slot of the BMP table. These pairs give the UnicodeData.txt simple value of each of
536         * those code points (a value equal to its key means "no simple mapping"). Every other
537         * code point's full mapping already is its simple mapping, including all supplementary
538         * ones (SpecialCasing has no supplementary entries). Generated from the vendored UCD
539         * files and checked against UnicodeData.txt for every code point by
540         * {@code GspCaseDataSimpleMappingTest}.
541         */
542        private static final char[] SIMPLE_UPPER_OVERRIDES = {
543            0x00DF, 0x00DF, 0x0149, 0x0149, 0x01F0, 0x01F0, 0x0390, 0x0390, 0x03B0, 0x03B0,
544            0x0587, 0x0587, 0x1E96, 0x1E96, 0x1E97, 0x1E97, 0x1E98, 0x1E98, 0x1E99, 0x1E99,
545            0x1E9A, 0x1E9A, 0x1F50, 0x1F50, 0x1F52, 0x1F52, 0x1F54, 0x1F54, 0x1F56, 0x1F56,
546            0x1F80, 0x1F88, 0x1F81, 0x1F89, 0x1F82, 0x1F8A, 0x1F83, 0x1F8B, 0x1F84, 0x1F8C,
547            0x1F85, 0x1F8D, 0x1F86, 0x1F8E, 0x1F87, 0x1F8F, 0x1F88, 0x1F88, 0x1F89, 0x1F89,
548            0x1F8A, 0x1F8A, 0x1F8B, 0x1F8B, 0x1F8C, 0x1F8C, 0x1F8D, 0x1F8D, 0x1F8E, 0x1F8E,
549            0x1F8F, 0x1F8F, 0x1F90, 0x1F98, 0x1F91, 0x1F99, 0x1F92, 0x1F9A, 0x1F93, 0x1F9B,
550            0x1F94, 0x1F9C, 0x1F95, 0x1F9D, 0x1F96, 0x1F9E, 0x1F97, 0x1F9F, 0x1F98, 0x1F98,
551            0x1F99, 0x1F99, 0x1F9A, 0x1F9A, 0x1F9B, 0x1F9B, 0x1F9C, 0x1F9C, 0x1F9D, 0x1F9D,
552            0x1F9E, 0x1F9E, 0x1F9F, 0x1F9F, 0x1FA0, 0x1FA8, 0x1FA1, 0x1FA9, 0x1FA2, 0x1FAA,
553            0x1FA3, 0x1FAB, 0x1FA4, 0x1FAC, 0x1FA5, 0x1FAD, 0x1FA6, 0x1FAE, 0x1FA7, 0x1FAF,
554            0x1FA8, 0x1FA8, 0x1FA9, 0x1FA9, 0x1FAA, 0x1FAA, 0x1FAB, 0x1FAB, 0x1FAC, 0x1FAC,
555            0x1FAD, 0x1FAD, 0x1FAE, 0x1FAE, 0x1FAF, 0x1FAF, 0x1FB2, 0x1FB2, 0x1FB3, 0x1FBC,
556            0x1FB4, 0x1FB4, 0x1FB6, 0x1FB6, 0x1FB7, 0x1FB7, 0x1FBC, 0x1FBC, 0x1FC2, 0x1FC2,
557            0x1FC3, 0x1FCC, 0x1FC4, 0x1FC4, 0x1FC6, 0x1FC6, 0x1FC7, 0x1FC7, 0x1FCC, 0x1FCC,
558            0x1FD2, 0x1FD2, 0x1FD3, 0x1FD3, 0x1FD6, 0x1FD6, 0x1FD7, 0x1FD7, 0x1FE2, 0x1FE2,
559            0x1FE3, 0x1FE3, 0x1FE4, 0x1FE4, 0x1FE6, 0x1FE6, 0x1FE7, 0x1FE7, 0x1FF2, 0x1FF2,
560            0x1FF3, 0x1FFC, 0x1FF4, 0x1FF4, 0x1FF6, 0x1FF6, 0x1FF7, 0x1FF7, 0x1FFC, 0x1FFC,
561            0xFB00, 0xFB00, 0xFB01, 0xFB01, 0xFB02, 0xFB02, 0xFB03, 0xFB03, 0xFB04, 0xFB04,
562            0xFB05, 0xFB05, 0xFB06, 0xFB06, 0xFB13, 0xFB13, 0xFB14, 0xFB14, 0xFB15, 0xFB15,
563            0xFB16, 0xFB16, 0xFB17, 0xFB17
564        };
565
566        /** See {@link #SIMPLE_UPPER_OVERRIDES}: only U+0130 (full lowercase {@code i̇}). */
567        private static final char[] SIMPLE_LOWER_OVERRIDES = {
568            0x0130, 0x0069
569        };
570
571        /**
572         * A copy of a full-mapping BMP table whose length-changing slots carry the simple
573         * mapping instead, so the simple operations run through the same 1:1 fast path. Fails
574         * fast when the override table and the resource disagree about which slots expand —
575         * that would mean the resource was regenerated from another UCD without this table.
576         */
577        private static char[] deriveSimpleBmp(char[] fullBmp, char[] overrides, String which) {
578            char[] simple = fullBmp.clone();
579            for (int i = 0; i < overrides.length; i += 2) {
580                char key = overrides[i];
581                char value = overrides[i + 1];
582                if (simple[key] != MULTI) {
583                    throw new IllegalStateException("Simple " + which + " override U+"
584                            + Integer.toHexString(key) + " is not an expanding slot of the "
585                            + UNICODE_VERSION + " case data");
586                }
587                simple[key] = value == key ? 0 : value;
588            }
589            for (int cp = 0; cp < simple.length; cp++) {
590                if (simple[cp] == MULTI) {
591                    throw new IllegalStateException("Expanding " + which + " slot U+"
592                            + Integer.toHexString(cp) + " has no simple override");
593                }
594            }
595            return simple;
596        }
597
598        /** blocks[b] = index of the first key >= b<<8; blocks[b+1] bounds the
599         *  slice, so a lookup binary-searches only its own 256-codepoint block. */
600        private static int[] blockIndex(int[] keys) {
601            int blockCount = (0x110000 >>> 8);
602            int[] blocks = new int[blockCount + 1];
603            int k = 0;
604            for (int b = 0; b <= blockCount; b++) {
605                int floor = b << 8;
606                while (k < keys.length && keys[k] < floor) {
607                    k++;
608                }
609                blocks[b] = k;
610            }
611            return blocks;
612        }
613
614        private static void deriveBmp(int[] keys, String[] values, char[] bmp) {
615            for (int i = 0; i < keys.length; i++) {
616                if (keys[i] <= 0xFFFF) {
617                    String v = values[i];
618                    bmp[keys[i]] = v.length() == 1 ? v.charAt(0) : MULTI;
619                }
620            }
621        }
622
623        /** All loading logic lives here, in ordinary verifiable methods. */
624        static Data load() {
625            InputStream raw = GspCaseData.class.getResourceAsStream("gsp-case-data.bin");
626            if (raw == null) {
627                throw new IllegalStateException("Missing case-data resource "
628                        + "gsp-case-data.bin next to " + GspCaseData.class.getName());
629            }
630            try {
631                DataInputStream in = new DataInputStream(raw);
632                try {
633                    byte[] magic = new byte[8];
634                    in.readFully(magic);
635                    if (!"GSPCASE1".equals(new String(magic, "US-ASCII"))) {
636                        throw new IllegalStateException("Bad case-data magic");
637                    }
638                    String version = in.readUTF();
639                    if (!UNICODE_VERSION.equals(version)) {
640                        throw new IllegalStateException("Case-data version " + version
641                                + " does not match compiled-in " + UNICODE_VERSION);
642                    }
643                    int rev = in.readInt();
644                    if (rev != ALGORITHM_REV) {
645                        throw new IllegalStateException("Case-data algorithm rev " + rev
646                                + " does not match compiled-in " + ALGORITHM_REV);
647                    }
648                    int n = in.readInt();
649                    int[] upperKeys = new int[n];
650                    String[] upperValues = new String[n];
651                    readMappingTable(in, n, upperKeys, upperValues);
652                    n = in.readInt();
653                    int[] lowerKeys = new int[n];
654                    String[] lowerValues = new String[n];
655                    readMappingTable(in, n, lowerKeys, lowerValues);
656                    n = in.readInt();
657                    int[] foldKeys = new int[n];
658                    int[] foldValues = new int[n];
659                    for (int i = 0; i < n; i++) {
660                        foldKeys[i] = in.readInt();
661                        foldValues[i] = in.readInt();
662                    }
663                    n = in.readInt();
664                    int[] casedStarts = new int[n];
665                    int[] casedEnds = new int[n];
666                    for (int i = 0; i < n; i++) {
667                        casedStarts[i] = in.readInt();
668                        casedEnds[i] = in.readInt();
669                    }
670                    n = in.readInt();
671                    int[] ignorableStarts = new int[n];
672                    int[] ignorableEnds = new int[n];
673                    for (int i = 0; i < n; i++) {
674                        ignorableStarts[i] = in.readInt();
675                        ignorableEnds[i] = in.readInt();
676                    }
677                    return new Data(upperKeys, upperValues, lowerKeys, lowerValues,
678                            foldKeys, foldValues, casedStarts, casedEnds,
679                            ignorableStarts, ignorableEnds);
680                } finally {
681                    in.close();
682                }
683            } catch (IOException e) {
684                throw new IllegalStateException("Failed to load gsp-case-data.bin", e);
685            }
686        }
687
688        private static void readMappingTable(DataInputStream in, int n,
689                                             int[] keys, String[] values) throws IOException {
690            for (int i = 0; i < n; i++) {
691                keys[i] = in.readInt();
692                int len = in.readUnsignedByte();
693                char[] chars = new char[len];
694                for (int j = 0; j < len; j++) {
695                    chars[j] = in.readChar();
696                }
697                values[i] = new String(chars);
698            }
699        }
700    }
701}