geoTokens.ts 2.6 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677
  1. import type { GeoKind } from '@/generated/types';
  2. const DEFAULT_SITE_FILE = 'geosite.dat';
  3. const DEFAULT_IP_FILE = 'geoip.dat';
  4. const LONG_FORMS: Array<[RegExp, string]> = [
  5. [/^ext(?:-domain|-site)?:geosite\.dat:/, 'geosite:'],
  6. [/^ext(?:-ip)?:geoip\.dat:/, 'geoip:'],
  7. ];
  8. export function parseTokens(value: string): string[] {
  9. return value
  10. .split(',')
  11. .map((token) => token.trim())
  12. .filter((token) => token !== '');
  13. }
  14. export function formatTokens(tokens: string[]): string {
  15. return tokens.join(', ');
  16. }
  17. export function tokenFor(file: string, code: string, kind: GeoKind): string {
  18. if (kind === 'ip' && file === DEFAULT_IP_FILE) return `geoip:${code}`;
  19. if (kind === 'site' && file === DEFAULT_SITE_FILE) return `geosite:${code}`;
  20. return `ext:${file}:${code}`;
  21. }
  22. /**
  23. * Xray treats category codes case-insensitively and accepts both the
  24. * `geosite:cn` shorthand and its `ext:geosite.dat:cn` long form, so tokens are
  25. * compared through this normal form. Only comparison uses it — whatever the
  26. * user typed is what stays in the rule.
  27. */
  28. export function canonicalToken(token: string): string {
  29. const lowered = token.trim().toLowerCase();
  30. for (const [pattern, shorthand] of LONG_FORMS) {
  31. if (pattern.test(lowered)) return lowered.replace(pattern, shorthand);
  32. }
  33. return lowered;
  34. }
  35. export function selectionFromValue(value: string, known: ReadonlySet<string>): string[] {
  36. const canonicalKnown = new Set([...known].map(canonicalToken));
  37. const selection: string[] = [];
  38. const seen = new Set<string>();
  39. for (const token of parseTokens(value)) {
  40. const canonical = canonicalToken(token);
  41. if (!canonicalKnown.has(canonical) || seen.has(canonical)) continue;
  42. seen.add(canonical);
  43. selection.push(token);
  44. }
  45. return selection;
  46. }
  47. export function mergeSelection(value: string, selected: string[], known: ReadonlySet<string>): string {
  48. const canonicalKnown = new Set([...known].map(canonicalToken));
  49. const kept = new Set(
  50. selected.map((token) => canonicalToken(token)).filter((token) => token !== ''),
  51. );
  52. const merged: string[] = [];
  53. const seen = new Set<string>();
  54. const append = (token: string) => {
  55. const canonical = canonicalToken(token);
  56. if (canonical === '' || seen.has(canonical)) return;
  57. seen.add(canonical);
  58. merged.push(token);
  59. };
  60. for (const token of parseTokens(value)) {
  61. const canonical = canonicalToken(token);
  62. if (canonicalKnown.has(canonical) && !kept.has(canonical)) continue;
  63. append(token);
  64. }
  65. for (const token of selected) {
  66. append(token.trim());
  67. }
  68. return formatTokens(merged);
  69. }