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>Three 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 * </ul>
030 *
031 * <p>Unpaired surrogates pass through unchanged in every operation — identity must be
032 * total on arbitrary UTF-16, and substitution would merge distinct malformed inputs.
033 * Inputs the operation does not change are returned as the SAME instance, including
034 * surrogate-bearing ones (zero allocation).
035 *
036 * <p>Thread-safe: one immutable table holder, safely published via the class
037 * initializer, whose body is straight-line code delegating to ordinary methods.
038 * (Historical note: this class's first obfuscated build failed verification because
039 * proguard.cfg once disabled preverification while emitting v52 class files — that
040 * affected EVERY branchy method product-wide and was fixed by re-enabling
041 * preverification; the branch-free initializer is retained as cheap hygiene.)
042 * Deployment notes: the resource is looked up relative to this class, so package
043 * RELOCATION (consumer-side shading) relocates the lookup with it, but the relocator
044 * must also move the resource directory; GraalVM native-image users must register
045 * {@code gudusoft/gsqlparser/sqlenv/unicode/gsp-case-data.bin} as a resource.
046 * Missing/corrupt resource fails fast with {@link ExceptionInInitializerError} on
047 * first use.
048 */
049public final class GspCaseData {
050
051    /** Exact UCD version the tables are generated from. Part of every policyId. */
052    public static final String UNICODE_VERSION = "16.0.0";
053
054    /** Bumped on ANY behavioral change of these operations. Part of every policyId. */
055    public static final int ALGORITHM_REV = 1;
056
057    private static final int SIGMA = 0x03A3;        // Σ
058    private static final int FINAL_SIGMA = 0x03C2;  // ς
059
060    // value 0 = no mapping (identity); MULTI = consult the sorted arrays.
061    // U+FFFF is a noncharacter and never a mapping target (asserted by the
062    // conformance suite against the audit dump), so it is a safe sentinel.
063    private static final char MULTI = (char) 0xFFFF;
064
065    /** Immutable table holder — loaded by ordinary (verifiable) methods; the
066     *  initializer below is pure straight-line code (no branches, no frames), and the
067     *  static-final aliases restore JIT constant folding lost to instance-field
068     *  indirection. */
069    private static final Data D = Data.load();
070    private static final int[] UPPER_KEYS = D.upperKeys;
071    private static final String[] UPPER_VALUES = D.upperValues;
072    private static final int[] LOWER_KEYS = D.lowerKeys;
073    private static final String[] LOWER_VALUES = D.lowerValues;
074    private static final int[] FOLD_KEYS = D.foldKeys;
075    private static final String[] FOLD_VALUE_STRINGS = D.foldValueStrings;
076    private static final int[] UPPER_BLOCKS = D.upperBlocks;
077    private static final int[] LOWER_BLOCKS = D.lowerBlocks;
078    private static final int[] FOLD_BLOCKS = D.foldBlocks;
079
080    /**
081     * Reusable mapping buffer for the 1:1 fast path (U5 fork-pair benchmark:
082     * the per-call {@code char[n]} plus {@code String}'s internal copy tripled
083     * allocation vs the JDK's in-{@code java.lang} zero-copy path). The buffer
084     * never escapes — {@code new String(buf, 0, n)} copies — and {@code map}
085     * neither recurses nor calls back into itself, so per-thread reuse is
086     * safe. Inputs longer than the buffer allocate as before.
087     */
088    private static final int SCRATCH_LIMIT = 256;
089    private static final ThreadLocal<char[]> SCRATCH = new ThreadLocal<char[]>() {
090        @Override
091        protected char[] initialValue() {
092            return new char[SCRATCH_LIMIT];
093        }
094    };
095    private static final char[] UPPER_BMP = D.upperBmp;
096    private static final char[] LOWER_BMP = D.lowerBmp;
097    private static final char[] FOLD_BMP = D.foldBmp;
098    private static final int[] CASED_STARTS = D.casedStarts;
099    private static final int[] CASED_ENDS = D.casedEnds;
100    private static final int[] IGNORABLE_STARTS = D.ignorableStarts;
101    private static final int[] IGNORABLE_ENDS = D.ignorableEnds;
102
103    private GspCaseData() {
104    }
105
106    /** Full uppercase mapping (unconditional UCD data; length may grow, e.g. ß→SS). */
107    public static String toUpperFull(String s) {
108        return map(s, UPPER_BMP, UPPER_KEYS, UPPER_BLOCKS, UPPER_VALUES, false);
109    }
110
111    /**
112     * Full lowercase mapping with the UCD Final_Sigma rule: Σ lowers to ς iff preceded
113     * by a Cased code point (skipping Case_Ignorable) and NOT followed by one (skipping
114     * Case_Ignorable). Everything else uses unconditional data.
115     */
116    public static String toLowerFull(String s) {
117        return map(s, LOWER_BMP, LOWER_KEYS, LOWER_BLOCKS, LOWER_VALUES, true);
118    }
119
120    /**
121     * CaseFolding C+S simple fold applied per code point — the canonical-equality
122     * collapse for INSENSITIVE + no-fold cells. Code-point-wise, so supplementary-plane
123     * pairs fold correctly (unlike the char-wise legacy collapse).
124     */
125    public static String simpleFoldCollapse(String s) {
126        return map(s, FOLD_BMP, FOLD_KEYS, FOLD_BLOCKS, FOLD_VALUE_STRINGS, false);
127    }
128
129    // ---- the mapper: ASCII tight path, then general code-point path ----
130
131    /**
132     * One mapper for all three operations; {@code finalSigma} enables the Σ context
133     * rule (lowercasing only). Two-tier shape (U5 gate 3): fixed points (nothing
134     * maps — the dominant identifier case) return the SAME instance with zero
135     * allocation, including surrogate-bearing inputs (valid pairs are probed
136     * against the supplementary keys; unpaired units are passthrough by policy).
137     * Strings whose mappings are all 1:1 in UTF-16 units — BMP single-char
138     * mappings, supplementary pairs mapping to one code point, and Σ (σ and ς are
139     * both one unit; the context scan reads the ORIGINAL string, so mapped output
140     * never affects it) — are built in a fixed {@code char[n]}. Only a length-
141     * changing mapping (MULTI expansions like ß→SS) drops to the StringBuilder
142     * walk, resuming exactly where the fast loop stopped.
143     */
144    private static String map(String s, char[] bmp, int[] keys, int[] blocks,
145                              String[] values, boolean finalSigma) {
146        if (s == null || s.isEmpty()) {
147            return s;
148        }
149        int n = s.length();
150        int first = firstMappedIndex(s, 0, bmp, keys, blocks);
151        if (first == n) {
152            return s; // fixed point: same instance, zero allocation
153        }
154        char[] out = n <= SCRATCH_LIMIT ? SCRATCH.get() : new char[n];
155        s.getChars(0, first, out, 0);
156        int j = first;
157        while (j < n) {
158            char d = s.charAt(j);
159            if (d < Character.MIN_SURROGATE || d > Character.MAX_SURROGATE) {
160                if (finalSigma && d == (char) SIGMA && isFinalSigmaContext(s, j)) {
161                    out[j] = (char) FINAL_SIGMA;
162                    j++;
163                    continue;
164                }
165                char md = bmp[d];
166                if (md == MULTI) {
167                    break; // length-changing expansion: finish on the slow path
168                }
169                out[j] = md == 0 ? d : md;
170                j++;
171                continue;
172            }
173            int cp = codePointOrUnit(s, j);
174            if (cp <= 0xFFFF) {
175                out[j] = d; // unpaired surrogate passthrough
176                j++;
177                continue;
178            }
179            int idx = lookup(keys, blocks, cp);
180            if (idx < 0) {
181                out[j] = d;
182                out[j + 1] = s.charAt(j + 1);
183                j += 2;
184                continue;
185            }
186            String v = values[idx];
187            if (v.length() != 2) {
188                break; // supplementary mapping that changes unit count: slow path
189            }
190            out[j] = v.charAt(0);
191            out[j + 1] = v.charAt(1);
192            j += 2;
193        }
194        if (j == n) {
195            return new String(out, 0, n);
196        }
197        StringBuilder b = new StringBuilder(n + 4);
198        b.append(out, 0, j);
199        return mapTail(s, j, b, bmp, keys, blocks, values, finalSigma);
200    }
201
202    /**
203     * Code-point lookup in a sorted keys table via its per-256-codepoint block
204     * index: jump straight to the (contiguous, cache-local) block slice and
205     * binary-search only inside it — replaces full-table binary search, whose
206     * ~11 scattered probes dominated supplementary-plane rows in the U5
207     * fork-pair benchmark (the "sparse per-plane lookup" review round 2
208     * suggested).
209     */
210    private static int lookup(int[] keys, int[] blocks, int cp) {
211        int b = cp >>> 8;
212        return java.util.Arrays.binarySearch(keys, blocks[b], blocks[b + 1], cp);
213    }
214
215    /** General walk from {@code i} appending to {@code b} (the pre-{@code i} text is
216     *  already appended). Handles MULTI expansions, supplementary keys, unpaired
217     *  surrogates, and — when {@code finalSigma} — the Σ context rule. */
218    private static String mapTail(String s, int i, StringBuilder b,
219                                  char[] bmp, int[] keys, int[] blocks,
220                                  String[] values, boolean finalSigma) {
221        int n = s.length();
222        while (i < n) {
223            char c = s.charAt(i);
224            if (c < Character.MIN_SURROGATE || c > Character.MAX_SURROGATE) {
225                if (finalSigma && c == (char) SIGMA && isFinalSigmaContext(s, i)) {
226                    b.append((char) FINAL_SIGMA);
227                    i++;
228                    continue;
229                }
230                char m = bmp[c];
231                if (m == 0) {
232                    b.append(c);
233                } else if (m != MULTI) {
234                    b.append(m);
235                } else {
236                    b.append(values[lookup(keys, blocks, c)]);
237                }
238                i++;
239                continue;
240            }
241            int cp = codePointOrUnit(s, i);
242            if (cp > 0xFFFF) {
243                int idx = lookup(keys, blocks, cp);
244                if (idx >= 0) {
245                    b.append(values[idx]);
246                } else {
247                    b.appendCodePoint(cp);
248                }
249                i += 2;
250            } else {
251                b.append((char) cp); // unpaired surrogate passthrough
252                i++;
253            }
254        }
255        return b.toString();
256    }
257
258    /**
259     * Index of the first UTF-16 position at or after {@code from} whose content the
260     * operation would map, or {@code s.length()} when the rest is a fixed point.
261     * Σ needs no special case: {@code LOWER_BMP[Σ]} is non-zero (σ), so a Σ always
262     * reports as mapped and the tail walk decides between σ and ς.
263     */
264    private static int firstMappedIndex(String s, int from, char[] bmp, int[] keys,
265                                        int[] blocks) {
266        int n = s.length();
267        int i = from;
268        while (i < n) {
269            char c = s.charAt(i);
270            if (c < Character.MIN_SURROGATE || c > Character.MAX_SURROGATE) {
271                if (bmp[c] != 0) {
272                    return i;
273                }
274                i++;
275                continue;
276            }
277            int cp = codePointOrUnit(s, i);
278            if (cp > 0xFFFF) {
279                if (lookup(keys, blocks, cp) >= 0) {
280                    return i;
281                }
282                i += 2;
283            } else {
284                i++; // unpaired surrogate: passthrough, keep scanning
285            }
286        }
287        return n;
288    }
289
290    // ---- Final_Sigma context (UCD definition, Cased/Case_Ignorable from DCP) ----
291
292    private static boolean isFinalSigmaContext(String s, int sigmaIndex) {
293        // Before: scanning backwards, skip Case_Ignorable; require Cased. Note the
294        // sets OVERLAP (U+0345 is both) — per the spec's possessive skipping, a
295        // code point that is Case_Ignorable is skipped regardless of Cased-ness.
296        int i = sigmaIndex;
297        boolean casedBefore = false;
298        while (i > 0) {
299            int cp = codePointBefore(s, i);
300            i -= cp > 0xFFFF ? 2 : 1;
301            if (isCaseIgnorable(cp)) {
302                continue;
303            }
304            casedBefore = isCased(cp);
305            break;
306        }
307        if (!casedBefore) {
308            return false;
309        }
310        // After: scanning forwards, skip Case_Ignorable; require NOT Cased.
311        i = sigmaIndex + 1; // Σ is a BMP char
312        while (i < s.length()) {
313            int cp = codePointOrUnit(s, i);
314            i += cp > 0xFFFF ? 2 : 1;
315            if (isCaseIgnorable(cp)) {
316                continue;
317            }
318            return !isCased(cp);
319        }
320        return true; // end of string after skipping ignorables
321    }
322
323    static boolean isCased(int cp) {
324        return inRanges(cp, CASED_STARTS, CASED_ENDS);
325    }
326
327    static boolean isCaseIgnorable(int cp) {
328        return inRanges(cp, IGNORABLE_STARTS, IGNORABLE_ENDS);
329    }
330
331    private static boolean inRanges(int cp, int[] starts, int[] ends) {
332        int idx = java.util.Arrays.binarySearch(starts, cp);
333        if (idx >= 0) {
334            return true;
335        }
336        int insertion = -idx - 2; // candidate range that starts before cp
337        return insertion >= 0 && cp <= ends[insertion];
338    }
339
340    // ---- UTF-16 walking that is total on malformed input ----
341
342    /** Code point at i, or the lone surrogate unit itself when unpaired. */
343    private static int codePointOrUnit(String s, int i) {
344        char c = s.charAt(i);
345        if (Character.isHighSurrogate(c) && i + 1 < s.length()
346                && Character.isLowSurrogate(s.charAt(i + 1))) {
347            return Character.toCodePoint(c, s.charAt(i + 1));
348        }
349        return c; // BMP char, or unpaired surrogate passed through as its unit value
350    }
351
352    private static int codePointBefore(String s, int i) {
353        char c = s.charAt(i - 1);
354        if (Character.isLowSurrogate(c) && i - 2 >= 0
355                && Character.isHighSurrogate(s.charAt(i - 2))) {
356            return Character.toCodePoint(s.charAt(i - 2), c);
357        }
358        return c;
359    }
360
361    // ---- the table holder: loaded by ordinary (verifiable) methods ----
362
363    private static final class Data {
364        final int[] upperKeys;
365        final String[] upperValues;
366        final int[] lowerKeys;
367        final String[] lowerValues;
368        final int[] foldKeys;
369        final String[] foldValueStrings;
370        final char[] upperBmp = new char[0x10000];
371        final char[] lowerBmp = new char[0x10000];
372        final char[] foldBmp = new char[0x10000];
373        final int[] casedStarts;
374        final int[] casedEnds;
375        final int[] ignorableStarts;
376        final int[] ignorableEnds;
377        final int[] upperBlocks;
378        final int[] lowerBlocks;
379        final int[] foldBlocks;
380
381        private Data(int[] upperKeys, String[] upperValues,
382                     int[] lowerKeys, String[] lowerValues,
383                     int[] foldKeys, int[] foldValues,
384                     int[] casedStarts, int[] casedEnds,
385                     int[] ignorableStarts, int[] ignorableEnds) {
386            this.upperKeys = upperKeys;
387            this.upperValues = upperValues;
388            this.lowerKeys = lowerKeys;
389            this.lowerValues = lowerValues;
390            this.foldKeys = foldKeys;
391            this.casedStarts = casedStarts;
392            this.casedEnds = casedEnds;
393            this.ignorableStarts = ignorableStarts;
394            this.ignorableEnds = ignorableEnds;
395            this.foldValueStrings = new String[foldValues.length];
396            for (int i = 0; i < foldValues.length; i++) {
397                StringBuilder fv = new StringBuilder(2);
398                fv.appendCodePoint(foldValues[i]);
399                this.foldValueStrings[i] = fv.toString();
400            }
401            deriveBmp(upperKeys, upperValues, upperBmp);
402            deriveBmp(lowerKeys, lowerValues, lowerBmp);
403            for (int i = 0; i < foldKeys.length; i++) {
404                if (foldKeys[i] <= 0xFFFF) {
405                    foldBmp[foldKeys[i]] = foldValues[i] <= 0xFFFF
406                            ? (char) foldValues[i] : MULTI;
407                }
408            }
409            this.upperBlocks = blockIndex(upperKeys);
410            this.lowerBlocks = blockIndex(lowerKeys);
411            this.foldBlocks = blockIndex(foldKeys);
412        }
413
414        /** blocks[b] = index of the first key >= b<<8; blocks[b+1] bounds the
415         *  slice, so a lookup binary-searches only its own 256-codepoint block. */
416        private static int[] blockIndex(int[] keys) {
417            int blockCount = (0x110000 >>> 8);
418            int[] blocks = new int[blockCount + 1];
419            int k = 0;
420            for (int b = 0; b <= blockCount; b++) {
421                int floor = b << 8;
422                while (k < keys.length && keys[k] < floor) {
423                    k++;
424                }
425                blocks[b] = k;
426            }
427            return blocks;
428        }
429
430        private static void deriveBmp(int[] keys, String[] values, char[] bmp) {
431            for (int i = 0; i < keys.length; i++) {
432                if (keys[i] <= 0xFFFF) {
433                    String v = values[i];
434                    bmp[keys[i]] = v.length() == 1 ? v.charAt(0) : MULTI;
435                }
436            }
437        }
438
439        /** All loading logic lives here, in ordinary verifiable methods. */
440        static Data load() {
441            InputStream raw = GspCaseData.class.getResourceAsStream("gsp-case-data.bin");
442            if (raw == null) {
443                throw new IllegalStateException("Missing case-data resource "
444                        + "gsp-case-data.bin next to " + GspCaseData.class.getName());
445            }
446            try {
447                DataInputStream in = new DataInputStream(raw);
448                try {
449                    byte[] magic = new byte[8];
450                    in.readFully(magic);
451                    if (!"GSPCASE1".equals(new String(magic, "US-ASCII"))) {
452                        throw new IllegalStateException("Bad case-data magic");
453                    }
454                    String version = in.readUTF();
455                    if (!UNICODE_VERSION.equals(version)) {
456                        throw new IllegalStateException("Case-data version " + version
457                                + " does not match compiled-in " + UNICODE_VERSION);
458                    }
459                    int rev = in.readInt();
460                    if (rev != ALGORITHM_REV) {
461                        throw new IllegalStateException("Case-data algorithm rev " + rev
462                                + " does not match compiled-in " + ALGORITHM_REV);
463                    }
464                    int n = in.readInt();
465                    int[] upperKeys = new int[n];
466                    String[] upperValues = new String[n];
467                    readMappingTable(in, n, upperKeys, upperValues);
468                    n = in.readInt();
469                    int[] lowerKeys = new int[n];
470                    String[] lowerValues = new String[n];
471                    readMappingTable(in, n, lowerKeys, lowerValues);
472                    n = in.readInt();
473                    int[] foldKeys = new int[n];
474                    int[] foldValues = new int[n];
475                    for (int i = 0; i < n; i++) {
476                        foldKeys[i] = in.readInt();
477                        foldValues[i] = in.readInt();
478                    }
479                    n = in.readInt();
480                    int[] casedStarts = new int[n];
481                    int[] casedEnds = new int[n];
482                    for (int i = 0; i < n; i++) {
483                        casedStarts[i] = in.readInt();
484                        casedEnds[i] = in.readInt();
485                    }
486                    n = in.readInt();
487                    int[] ignorableStarts = new int[n];
488                    int[] ignorableEnds = new int[n];
489                    for (int i = 0; i < n; i++) {
490                        ignorableStarts[i] = in.readInt();
491                        ignorableEnds[i] = in.readInt();
492                    }
493                    return new Data(upperKeys, upperValues, lowerKeys, lowerValues,
494                            foldKeys, foldValues, casedStarts, casedEnds,
495                            ignorableStarts, ignorableEnds);
496                } finally {
497                    in.close();
498                }
499            } catch (IOException e) {
500                throw new IllegalStateException("Failed to load gsp-case-data.bin", e);
501            }
502        }
503
504        private static void readMappingTable(DataInputStream in, int n,
505                                             int[] keys, String[] values) throws IOException {
506            for (int i = 0; i < n; i++) {
507                keys[i] = in.readInt();
508                int len = in.readUnsignedByte();
509                char[] chars = new char[len];
510                for (int j = 0; j < len; j++) {
511                    chars[j] = in.readChar();
512                }
513                values[i] = new String(chars);
514            }
515        }
516    }
517}