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}