@pipeworx/mcp-edge-case-museum 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/README.md +17 -12
  2. package/bin/cli.js +55 -11
  3. package/lib/index.js +333 -0
  4. package/package.json +17 -16
  5. package/server.json +1 -1
  6. package/tools.json +98 -0
  7. package/src/bake.mjs +0 -112
  8. package/src/ecm-index-data.ts +0 -15
  9. package/src/exhibits/datetime/add-24-hours-vs-add-1-day-across-dst.json +0 -51
  10. package/src/exhibits/datetime/add-365-days-to-feb-29-not-next-feb-29.json +0 -49
  11. package/src/exhibits/datetime/age-calculator-feb29-birthday-crash-vs-silent.json +0 -52
  12. package/src/exhibits/datetime/century-leap-year-1900-not-leap.json +0 -68
  13. package/src/exhibits/datetime/datetime-utcnow-naive-deprecated.json +0 -39
  14. package/src/exhibits/datetime/dst-fall-back-ambiguous-fold.json +0 -51
  15. package/src/exhibits/datetime/dst-spring-forward-nonexistent-time.json +0 -57
  16. package/src/exhibits/datetime/fractional-utc-offsets-kathmandu-kolkata.json +0 -50
  17. package/src/exhibits/datetime/iso-week-year-vs-calendar-year.json +0 -48
  18. package/src/exhibits/datetime/js-date-field-overflow-rollover.json +0 -49
  19. package/src/exhibits/datetime/leap-second-rejected-vs-silently-invalid.json +0 -57
  20. package/src/exhibits/datetime/lord-howe-30-minute-dst.json +0 -47
  21. package/src/exhibits/datetime/midnight-that-doesnt-exist-sao-paulo.json +0 -53
  22. package/src/exhibits/datetime/python-naive-aware-comparison-typeerror.json +0 -46
  23. package/src/exhibits/datetime/samoa-skipped-day-2011.json +0 -50
  24. package/src/exhibits/datetime/southern-hemisphere-dst-reversed.json +0 -50
  25. package/src/exhibits/datetime/two-digit-year-century-pivot-mismatch.json +0 -49
  26. package/src/exhibits/datetime/year-2038-int32-overflow.json +0 -52
  27. package/src/exhibits/filenames/absolute-second-argument-resets-join.json +0 -62
  28. package/src/exhibits/filenames/bidi-override-extension-spoof.json +0 -58
  29. package/src/exhibits/filenames/case-insensitive-filesystem-pure-path-mismatch.json +0 -74
  30. package/src/exhibits/filenames/dotfile-has-no-extension.json +0 -61
  31. package/src/exhibits/filenames/extension-trap-double-extension.json +0 -62
  32. package/src/exhibits/filenames/filename-length-limits-bytes-not-chars.json +0 -66
  33. package/src/exhibits/filenames/leading-dash-filename-parsed-as-flag.json +0 -67
  34. package/src/exhibits/filenames/nfc-nfd-filename-lookup-mismatch.json +0 -68
  35. package/src/exhibits/filenames/ntfs-alternate-data-stream-colon.json +0 -59
  36. package/src/exhibits/filenames/ntpath-splitdrive-unc-paths.json +0 -58
  37. package/src/exhibits/filenames/nul-byte-in-filename-raises-at-open.json +0 -60
  38. package/src/exhibits/filenames/path-join-escapes-the-base-directory.json +0 -63
  39. package/src/exhibits/filenames/windows-backslash-traversal-ntpath.json +0 -62
  40. package/src/exhibits/filenames/windows-reserved-device-names.json +0 -73
  41. package/src/exhibits/filenames/windows-trailing-dot-space-stripped.json +0 -77
  42. package/src/exhibits/float-money/decimal-from-float-vs-string.json +0 -66
  43. package/src/exhibits/float-money/float-anchor-0-1-plus-0-2.json +0 -67
  44. package/src/exhibits/float-money/intl-number-format-de-de-roundtrip-misparsed.json +0 -62
  45. package/src/exhibits/float-money/iso4217-minor-units-not-always-two.json +0 -78
  46. package/src/exhibits/float-money/math-abs-negative-zero.json +0 -51
  47. package/src/exhibits/float-money/max-safe-integer-plus-two.json +0 -62
  48. package/src/exhibits/float-money/naive-loop-sum-order-dependence.json +0 -71
  49. package/src/exhibits/float-money/nan-breaks-equality-and-max-min.json +0 -70
  50. package/src/exhibits/float-money/negative-zero-balance-formatting.json +0 -74
  51. package/src/exhibits/float-money/number-epsilon-false-positive-small-magnitude.json +0 -52
  52. package/src/exhibits/float-money/parsefloat-comma-thousands-separator.json +0 -66
  53. package/src/exhibits/float-money/python-banker-rounding-vs-js-round-half-up.json +0 -65
  54. package/src/exhibits/float-money/python-round-2-675-float-imprecision.json +0 -64
  55. package/src/exhibits/float-money/splitting-100-dollars-three-ways-leaves-a-cent.json +0 -63
  56. package/src/exhibits/float-money/sum-ten-dimes-not-a-dollar.json +0 -77
  57. package/src/exhibits/float-money/tofixed-1-005-rounds-down-not-up.json +0 -63
  58. package/src/exhibits/float-money/tofixed-2-345-inconsistent-rounding.json +0 -64
  59. package/src/exhibits/integers/array-index-above-2-32-2-not-an-index.json +0 -42
  60. package/src/exhibits/integers/bigint-number-mixing-typeerror.json +0 -49
  61. package/src/exhibits/integers/bitwise-32-bit-truncation.json +0 -56
  62. package/src/exhibits/integers/bool-is-int-subclass-dict-key-collision.json +0 -58
  63. package/src/exhibits/integers/floor-div-mod-sign-python-vs-js.json +0 -48
  64. package/src/exhibits/integers/json-parse-loses-id-precision.json +0 -58
  65. package/src/exhibits/integers/max-safe-integer-plus-one-equals-plus-two.json +0 -58
  66. package/src/exhibits/integers/number-empty-and-whitespace-string-equals-zero.json +0 -55
  67. package/src/exhibits/integers/parseint-null-radix36-digit-soup.json +0 -57
  68. package/src/exhibits/integers/parseint-number-argument-scientific-notation.json +0 -48
  69. package/src/exhibits/integers/parseint-stops-at-first-invalid-char-vs-number.json +0 -58
  70. package/src/exhibits/integers/python-int-accepts-fullwidth-unicode-digits.json +0 -56
  71. package/src/exhibits/integers/python-int-accepts-underscores-and-whitespace.json +0 -48
  72. package/src/exhibits/integers/python-unbounded-int-vs-fixed-width-wrap.json +0 -49
  73. package/src/exhibits/names-addresses/address-with-no-street-number.json +0 -54
  74. package/src/exhibits/names-addresses/ascii-regex-rejects-accented-and-hyphenated-names.json +0 -55
  75. package/src/exhibits/names-addresses/house-number-suffix-collapsed-by-digit-extraction.json +0 -55
  76. package/src/exhibits/names-addresses/mononym-fails-first-last-required.json +0 -56
  77. package/src/exhibits/names-addresses/naive-lowercase-breaks-greek-final-sigma.json +0 -64
  78. package/src/exhibits/names-addresses/null-surname-coerced-to-null-value.json +0 -58
  79. package/src/exhibits/names-addresses/single-letter-surname-fails-min-length.json +0 -55
  80. package/src/exhibits/names-addresses/sort-by-surname-particle-convention-mismatch.json +0 -57
  81. package/src/exhibits/names-addresses/spanish-double-surname-treated-as-single.json +0 -55
  82. package/src/exhibits/names-addresses/split-surname-on-last-space-loses-particle.json +0 -59
  83. package/src/exhibits/names-addresses/title-case-mangles-mixed-case-and-apostrophes.json +0 -60
  84. package/src/exhibits/names-addresses/uk-canada-postal-code-assumed-numeric.json +0 -55
  85. package/src/exhibits/names-addresses/us-zip-code-leading-zero-lost-as-integer.json +0 -58
  86. package/src/exhibits/names-addresses/varchar-truncation-splits-name-mid-grapheme.json +0 -63
  87. package/src/exhibits/parsing/csv-crlf-trailing-blank-line.json +0 -87
  88. package/src/exhibits/parsing/csv-doubled-quote-escaping.json +0 -70
  89. package/src/exhibits/parsing/csv-formula-injection-excel.json +0 -71
  90. package/src/exhibits/parsing/csv-quoted-field-with-comma.json +0 -70
  91. package/src/exhibits/parsing/csv-quoted-field-with-newline.json +0 -70
  92. package/src/exhibits/parsing/csv-semicolon-delimited-european-excel.json +0 -70
  93. package/src/exhibits/parsing/csv-utf8-bom-first-header.json +0 -74
  94. package/src/exhibits/parsing/email-regex-rejects-valid-apostrophe-address.json +0 -68
  95. package/src/exhibits/parsing/json-big-integer-precision-loss.json +0 -65
  96. package/src/exhibits/parsing/json-duplicate-keys-last-wins.json +0 -64
  97. package/src/exhibits/parsing/json-lone-surrogate-roundtrip.json +0 -58
  98. package/src/exhibits/parsing/json-nan-infinity-not-valid-json.json +0 -54
  99. package/src/exhibits/parsing/url-backslash-treated-as-slash-whatwg.json +0 -60
  100. package/src/exhibits/parsing/url-urlparse-never-raises-on-garbage.json +0 -60
  101. package/src/exhibits/parsing/url-userinfo-trusted-looking-host.json +0 -60
  102. package/src/exhibits/sorting/accented-letters-sort-after-z.json +0 -63
  103. package/src/exhibits/sorting/byte-order-caps-before-lowercase.json +0 -66
  104. package/src/exhibits/sorting/case-insensitive-sort-lower-vs-casefold.json +0 -56
  105. package/src/exhibits/sorting/comparator-returns-boolean-js.json +0 -49
  106. package/src/exhibits/sorting/js-array-sort-stringifies-numbers.json +0 -68
  107. package/src/exhibits/sorting/locale-collation-de-vs-sv.json +0 -54
  108. package/src/exhibits/sorting/localecompare-numeric-option.json +0 -50
  109. package/src/exhibits/sorting/nan-in-sort.json +0 -59
  110. package/src/exhibits/sorting/natural-sort-breaks-lexicographic.json +0 -63
  111. package/src/exhibits/sorting/postgres-collation-documented.json +0 -61
  112. package/src/exhibits/sorting/python-cannot-compare-str-int.json +0 -70
  113. package/src/exhibits/sorting/reverse-sort-vs-reverse-after-sort-ties.json +0 -48
  114. package/src/exhibits/sorting/sorting-mixed-none-null.json +0 -56
  115. package/src/exhibits/sorting/typed-array-sort-is-numeric-by-default.json +0 -48
  116. package/src/exhibits/unicode/arabic-indic-digit-parses.json +0 -73
  117. package/src/exhibits/unicode/bidi-override-filename-spoof.json +0 -62
  118. package/src/exhibits/unicode/cyrillic-homoglyph-paypal-spoof.json +0 -67
  119. package/src/exhibits/unicode/fullwidth-bypasses-keyword-filter.json +0 -61
  120. package/src/exhibits/unicode/german-eszett-case-fold.json +0 -72
  121. package/src/exhibits/unicode/grapheme-vs-codepoint-length.json +0 -57
  122. package/src/exhibits/unicode/greek-final-sigma-casefold.json +0 -54
  123. package/src/exhibits/unicode/js-word-regex-ascii-only.json +0 -64
  124. package/src/exhibits/unicode/nfc-nfd-same-name-two-accounts.json +0 -70
  125. package/src/exhibits/unicode/nfkc-compatibility-folding.json +0 -63
  126. package/src/exhibits/unicode/truncation-orphans-combining-mark.json +0 -62
  127. package/src/exhibits/unicode/truncation-splits-surrogate-pair.json +0 -65
  128. package/src/exhibits/unicode/turkish-dotless-i-case-mapping.json +0 -72
  129. package/src/exhibits/unicode/zalgo-combining-mark-stacking.json +0 -57
  130. package/src/exhibits/unicode/zero-width-space-username.json +0 -62
  131. package/src/harness.mjs +0 -331
  132. package/src/index.ts +0 -686
  133. package/src/server.ts +0 -49
  134. package/src/verified.json +0 -977
  135. package/tsconfig.json +0 -18
@@ -1,54 +0,0 @@
1
- {
2
- "id": "json-nan-infinity-not-valid-json",
3
- "title": "Python's json.dumps writes NaN and Infinity, which are not JSON - json.loads reads them back, JSON.parse throws",
4
- "category": "parsing",
5
- "assumption": "Whatever json.dumps() produces is, by definition, valid JSON that any JSON parser can read back.",
6
- "input": {
7
- "display": "json.dumps(float(\"nan\")) and json.dumps(float(\"inf\"))",
8
- "description": "A computed statistic, average, or ratio that happens to divide by zero or otherwise become NaN/Infinity, serialized directly into an API response or log line without checking first."
9
- },
10
- "what_goes_wrong": "`json.dumps(float(\"nan\"))` returns the string `\"NaN\"` and `json.dumps(float(\"inf\"))` returns `\"Infinity\"` - neither token exists in the JSON grammar, so a strict JSON parser (including JavaScript's own `JSON.parse`) throws a syntax error reading data that Python itself happily produced and sent.",
11
- "why": "The JSON spec (RFC 8259) defines numbers using only digits, an optional sign, decimal point, and exponent - `NaN` and `Infinity` are not part of that grammar. Python's `json.dumps` defaults to `allow_nan=True`, which intentionally emits the non-standard literals `NaN`, `Infinity`, and `-Infinity` for those float values rather than raising an error, prioritizing round-tripping Python floats over strict JSON compliance. `json.loads` on the Python side will happily read its own non-standard output back, which hides the incompatibility until something stricter - a browser's `JSON.parse`, another language's parser, a schema validator - receives it.",
12
- "affects": [
13
- "python",
14
- "javascript"
15
- ],
16
- "repros": [
17
- {
18
- "language": "python",
19
- "code": "import json\ns_nan = json.dumps(float(\"nan\"))\ns_inf = json.dumps(float(\"inf\"))\nprint(s_nan, s_inf)\nprint(json.loads(s_nan))\ntry:\n json.dumps(float(\"nan\"), allow_nan=False)\nexcept ValueError as e:\n print(\"ValueError:\", e)\n",
20
- "expected_output": "NaN Infinity\nnan\nValueError: Out of range float values are not JSON compliant: nan",
21
- "verified_with": "Python 3.14.2"
22
- },
23
- {
24
- "language": "javascript",
25
- "code": "try {\n JSON.parse(\"NaN\");\n} catch (e) {\n console.log(\"THROWS:\", e.message);\n}\ntry {\n JSON.parse(\"Infinity\");\n} catch (e) {\n console.log(\"THROWS:\", e.message);\n}\nconsole.log(JSON.stringify(NaN), JSON.stringify(Infinity));\n",
26
- "expected_output": "THROWS: \"NaN\" is not valid JSON\nTHROWS: \"Infinity\" is not valid JSON\nnull null",
27
- "verified_with": "Node.js v22.22.2"
28
- }
29
- ],
30
- "fix": "Call `json.dumps(value, allow_nan=False)` so Python raises immediately instead of emitting non-standard JSON, and decide explicitly how to represent a NaN/Infinity result (e.g. `null`, a sentinel string, or omit the field) before it reaches a serializer. Note JavaScript's own `JSON.stringify` already does the opposite - it silently turns `NaN`/`Infinity` into `null` rather than erroring - so the two languages fail in different directions here.",
31
- "related": [
32
- "json-big-integer-precision-loss"
33
- ],
34
- "sources": [
35
- {
36
- "title": "RFC 8259: The JavaScript Object Notation (JSON) Data Interchange Format",
37
- "url": "https://www.rfc-editor.org/rfc/rfc8259"
38
- },
39
- {
40
- "title": "Python docs: json - JSON encoder and decoder (allow_nan)",
41
- "url": "https://docs.python.org/3/library/json.html#json.dump"
42
- },
43
- {
44
- "title": "MDN: JSON.stringify()",
45
- "url": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify"
46
- }
47
- ],
48
- "tags": [
49
- "json",
50
- "parsing",
51
- "validation",
52
- "money"
53
- ]
54
- }
@@ -1,60 +0,0 @@
1
- {
2
- "id": "url-backslash-treated-as-slash-whatwg",
3
- "title": "A backslash in an http(s) URL is silently treated as a forward slash by the browser's URL parser, but not by Python",
4
- "category": "parsing",
5
- "assumption": "A URL parser only treats \"/\" as a path separator, so a backslash in a URL string stays part of the host or path text it was written in.",
6
- "input": {
7
- "display": "http://example.com\\evil.com/path",
8
- "description": "A URL built by concatenating a trusted base domain with user-controlled text that includes a backslash - deliberately, as an SSRF or open-redirect bypass attempt, or accidentally, from a Windows-style path pasted into a URL field."
9
- },
10
- "what_goes_wrong": "The WHATWG `URL` parser (used by every modern browser and by Node's global `URL`) treats the backslash exactly like a forward slash for `http`/`https` URLs, splitting the host at it: `new URL(\"http://example.com\\\\evil.com/path\").hostname` is `\"example.com\"` with pathname `\"/evil.com/path\"`. Python's `urllib.parse.urlparse` does not special-case backslashes at all, so it keeps `\"example.com\\\\evil.com\"` as one literal hostname string. The same input string resolves to a completely different host depending on which parser read it - a host-based allowlist check written against one parser's output can disagree with the HTTP client (or browser) that actually makes the request using the other's interpretation.",
11
- "why": "The WHATWG URL Standard treats `\\` as equivalent to `/` only for \"special\" schemes (http, https, ws, wss, ftp, file), recording it as a validation error but continuing, because browsers historically normalized backslashes for compatibility with sloppy real-world URLs. For a non-special scheme the backslash is not a separator: `new URL(\"foo://example.com/a\\\\b\").pathname` keeps it as `/a\\\\b`. Python's `urllib.parse` follows the RFC 3986 generic syntax, in which a backslash is not a legal URI character at all - but `urlparse` does no character validation, so it simply leaves the backslash inside whatever component it lands in. Neither parser is misbehaving by its own spec; they just answer \"what host is this?\" differently for the same string.",
12
- "affects": [
13
- "javascript",
14
- "any WHATWG-URL parser (browsers, Node's URL)",
15
- "python (as the disagreeing side: urllib.parse keeps the backslash in the host)"
16
- ],
17
- "repros": [
18
- {
19
- "language": "javascript",
20
- "code": "const u = new URL(\"http://example.com\\\\evil.com/path\");\nconsole.log(u.hostname, u.pathname);\n// only special schemes: a non-special scheme keeps the backslash in the path\nconst f = new URL(\"foo://example.com/a\\\\b\");\nconst h = new URL(\"http://example.com/a\\\\b\");\nconsole.log(f.pathname, h.pathname);\n",
21
- "expected_output": "example.com /evil.com/path\n/a\\b /a/b",
22
- "verified_with": "Node.js v22.22.2"
23
- },
24
- {
25
- "language": "python",
26
- "code": "from urllib.parse import urlparse\nu = urlparse(\"http://example.com\\\\evil.com/path\")\nprint(u.hostname, u.path)\n",
27
- "expected_output": "example.com\\evil.com /path",
28
- "verified_with": "Python 3.14.2"
29
- }
30
- ],
31
- "fix": "Never build a trust decision (an SSRF allowlist, a same-origin check) against a hostname string parsed by one library if the request will actually be issued by a different URL-parsing implementation (a browser, a different HTTP client, another language's service). Normalize backslashes to forward slashes (or reject URLs containing them) before any parsing happens, consistently across every component that will touch the URL.",
32
- "related": [
33
- "url-userinfo-trusted-looking-host"
34
- ],
35
- "sources": [
36
- {
37
- "title": "WHATWG URL Standard: special schemes and path-segment separators",
38
- "url": "https://url.spec.whatwg.org/#url-parsing"
39
- },
40
- {
41
- "title": "Python docs: urllib.parse",
42
- "url": "https://docs.python.org/3/library/urllib.parse.html"
43
- }
44
- ],
45
- "tags": [
46
- "url",
47
- "parsing",
48
- "security",
49
- "validation"
50
- ],
51
- "test_cases": [
52
- {
53
- "inputs": [
54
- "http://example.com\\evil.com/path"
55
- ],
56
- "relation": "no_crash",
57
- "note": "their_extract_hostname(url) must be checked against the SAME parser that will actually dereference the URL - a hostname check written against one URL parser can be bypassed by a client that resolves backslashes differently."
58
- }
59
- ]
60
- }
@@ -1,60 +0,0 @@
1
- {
2
- "id": "url-urlparse-never-raises-on-garbage",
3
- "title": "Python's urlparse doesn't raise on garbage - \"not a url\" parses \"successfully\" into an empty scheme and host",
4
- "category": "parsing",
5
- "assumption": "If a string isn't a valid URL, the URL-parsing function you call will raise an error, so \"it didn't throw\" is good evidence the input was a real URL.",
6
- "input": {
7
- "display": "not a url",
8
- "description": "User-submitted text in a field that is supposed to be a URL (a profile link, a webhook target, a redirect destination), validated with `urlparse` and a check like `if result.netloc: ...` or simply `try: urlparse(x) except: reject`."
9
- },
10
- "what_goes_wrong": "`urlparse(\"not a url\")` does not raise - it returns a `ParseResult` with empty `scheme` and `netloc` and the whole input shoved into `.path`. JavaScript's `new URL(\"not a url\")` does the opposite: it throws `TypeError: Invalid URL` immediately, because the WHATWG algorithm requires an absolute URL (or a base to resolve against) and fails loudly otherwise.",
11
- "why": "Python's `urllib.parse.urlparse` implements RFC 3986's generic syntax as a permissive, almost mechanical split on delimiter characters (`://`, `@`, `:`, `/`, `?`, `#`) - if the expected delimiters for scheme/netloc aren't present, it doesn't fail, it just leaves those fields empty and puts everything into whichever component comes last in the fallback order (`path`). It was designed to parse URI *references* (which may be relative, schemeless fragments) permissively, not to validate that a string is a complete, dereferenceable URL. The WHATWG `URL` constructor is deliberately the opposite: it exists specifically to parse (and validate) a resolvable URL, so it throws on anything that doesn't resolve to one. (It is not literally exception-free: an unbalanced IPv6 bracket such as `http://[::1` raises ValueError, and reading `.port` raises for an out-of-range port - but ordinary non-URL text goes straight through.)",
12
- "affects": [
13
- "python"
14
- ],
15
- "repros": [
16
- {
17
- "language": "python",
18
- "code": "from urllib.parse import urlparse\nr = urlparse(\"not a url\")\nprint(r)\nprint(bool(r.scheme), bool(r.netloc))\n",
19
- "expected_output": "ParseResult(scheme='', netloc='', path='not a url', params='', query='', fragment='')\nFalse False",
20
- "verified_with": "Python 3.14.2"
21
- },
22
- {
23
- "language": "javascript",
24
- "code": "try {\n const u = new URL(\"not a url\");\n console.log(\"parsed\", u.href);\n} catch (e) {\n console.log(\"THROWS:\", e.message);\n}\n",
25
- "expected_output": "THROWS: Invalid URL",
26
- "verified_with": "Node.js v22.22.2"
27
- }
28
- ],
29
- "fix": "Never treat \"urlparse didn't raise\" as proof of a valid URL in Python. Explicitly check that both `scheme` is one of the schemes you accept (e.g. `http`/`https`) and `netloc` is non-empty, or use a stricter validator before trusting the result.",
30
- "related": [
31
- "url-backslash-treated-as-slash-whatwg",
32
- "url-userinfo-trusted-looking-host"
33
- ],
34
- "sources": [
35
- {
36
- "title": "Python docs: urllib.parse",
37
- "url": "https://docs.python.org/3/library/urllib.parse.html"
38
- },
39
- {
40
- "title": "WHATWG URL Standard: URL parsing failures",
41
- "url": "https://url.spec.whatwg.org/#url-parsing"
42
- }
43
- ],
44
- "tags": [
45
- "url",
46
- "parsing",
47
- "validation",
48
- "forms",
49
- "security"
50
- ],
51
- "test_cases": [
52
- {
53
- "inputs": [
54
- "not a url"
55
- ],
56
- "relation": "no_crash",
57
- "note": "their_is_valid_url(text) must explicitly reject this - checking only whether urlparse() raised is not enough, since urlparse never raises on this input."
58
- }
59
- ]
60
- }
@@ -1,60 +0,0 @@
1
- {
2
- "id": "url-userinfo-trusted-looking-host",
3
- "title": "\"http://trusted.com@evil.com/\" goes to evil.com, not trusted.com - and both languages' real parsers already know that",
4
- "category": "parsing",
5
- "assumption": "A URL string that starts with (or contains) \"trusted.com\" points at trusted.com.",
6
- "input": {
7
- "display": "http://trusted.com@evil.com/",
8
- "description": "A link in a phishing email, a redirect parameter, or a webhook target URL that a naive check validates by looking for a trusted domain name somewhere in the string - `url.startsWith(\"http://trusted.com\")` or a regex containing `trusted\\.com`."
9
- },
10
- "what_goes_wrong": "A substring/prefix check for \"trusted.com\" passes on `http://trusted.com@evil.com/`, but the browser (or any real HTTP client) actually connects to `evil.com` - `trusted.com` here is just userinfo (a username with no password), not the host, and both a proper WHATWG `URL` parse and Python's `urlparse` agree the real host is `evil.com`.",
11
- "why": "RFC 3986's authority component has the shape `userinfo@host:port` - everything before an `@` in the authority is credentials, not part of the hostname, and both a real URL parser and the browser's address bar logic strip it before deciding where to connect. A regex or `startsWith` check that only looks for the trusted domain's text anywhere near the front of the string has no concept of URL grammar at all, so it is fooled by a syntax feature (`@`) that every conformant parser already understands correctly. This is not a parser disagreement - it's a gap between \"looks like it mentions the right domain\" and \"actually parses to the right host\".",
12
- "affects": [
13
- "python",
14
- "javascript",
15
- "any string-based (non-parsing) URL trust check"
16
- ],
17
- "repros": [
18
- {
19
- "language": "javascript",
20
- "code": "const u = new URL(\"http://trusted.com@evil.com/\");\nconsole.log(u.hostname, u.username);\nconsole.log(\"http://trusted.com@evil.com/\".startsWith(\"http://trusted.com\"));\n",
21
- "expected_output": "evil.com trusted.com\ntrue",
22
- "verified_with": "Node.js v22.22.2"
23
- },
24
- {
25
- "language": "python",
26
- "code": "from urllib.parse import urlparse\nu = urlparse(\"http://trusted.com@evil.com/\")\nprint(u.hostname, u.username)\nprint(\"http://trusted.com@evil.com/\".startswith(\"http://trusted.com\"))\n",
27
- "expected_output": "evil.com trusted.com\nTrue",
28
- "verified_with": "Python 3.14.2"
29
- }
30
- ],
31
- "fix": "Never validate a URL's target by substring or prefix matching on the raw string. Parse the URL and compare the parsed `.hostname` against an allowlist of exact hostnames (or a proper suffix match on the hostname component specifically, e.g. ends with `.trusted.com`), never against the whole URL text.",
32
- "related": [
33
- "url-backslash-treated-as-slash-whatwg"
34
- ],
35
- "sources": [
36
- {
37
- "title": "RFC 3986: Uniform Resource Identifier (URI): Generic Syntax, Section 3.2",
38
- "url": "https://www.rfc-editor.org/rfc/rfc3986#section-3.2"
39
- },
40
- {
41
- "title": "WHATWG URL Standard",
42
- "url": "https://url.spec.whatwg.org/"
43
- }
44
- ],
45
- "tags": [
46
- "url",
47
- "parsing",
48
- "security",
49
- "validation"
50
- ],
51
- "test_cases": [
52
- {
53
- "inputs": [
54
- "http://trusted.com@evil.com/"
55
- ],
56
- "relation": "no_crash",
57
- "note": "their_is_trusted_url(url) must parse the URL and check the hostname field - not check whether the string contains or starts with the trusted domain's text - or this input will pass a check it should fail."
58
- }
59
- ]
60
- }
@@ -1,63 +0,0 @@
1
- {
2
- "id": "accented-letters-sort-after-z",
3
- "title": "\"äpple\" sorts after \"zebra\" with a default sort",
4
- "category": "sorting",
5
- "assumption": "An accented letter sorts next to its unaccented counterpart — \"äpple\" belongs near \"apple\", not off at the end of the alphabet.",
6
- "input": {
7
- "display": "[\"zebra\", \"äpple\", \"apple\"]",
8
- "description": "A product or name list containing a word with a diacritic, sorted with the default (non-locale-aware) string comparison."
9
- },
10
- "what_goes_wrong": "The default sort returns [\"apple\", \"zebra\", \"äpple\"] — \"äpple\" lands after \"zebra\", not next to \"apple\" where a human alphabetizing by the base letter would put it.",
11
- "why": "\"ä\" is U+00E4, a higher value than \"z\" (U+007A): the Latin-1 Supplement block, where most Western European accented letters live, comes entirely after the ASCII lowercase letters. Python's default string comparison orders by code point and JavaScript's by UTF-16 code unit (the same thing for these characters), so neither knows that \"ä\" is a variant of \"a\" in most European alphabets. Only a locale-aware collator knows that relationship.",
12
- "affects": [
13
- "python",
14
- "javascript"
15
- ],
16
- "repros": [
17
- {
18
- "language": "python",
19
- "code": "words = [\"zebra\", \"\\u00e4pple\", \"apple\"]\nprint(sorted(words))\n",
20
- "expected_output": "['apple', 'zebra', 'äpple']",
21
- "verified_with": "Python 3.14.2"
22
- },
23
- {
24
- "language": "javascript",
25
- "code": "const words = [\"zebra\", \"\\u00e4pple\", \"apple\"];\nconsole.log(words.slice().sort());\nconsole.log(words.slice().sort((a, b) => a.localeCompare(b)));\n",
26
- "expected_output": "[ 'apple', 'zebra', 'äpple' ]\n[ 'apple', 'äpple', 'zebra' ]",
27
- "verified_with": "Node.js v22.22.2"
28
- }
29
- ],
30
- "fix": "Use a locale-aware comparator for any text a human will read: Intl.Collator or String.prototype.localeCompare() in JavaScript. Python's standard library has no built-in Unicode collation; it requires either the system locale module (environment-dependent) or a third-party ICU binding.",
31
- "related": [
32
- "byte-order-caps-before-lowercase",
33
- "locale-collation-de-vs-sv"
34
- ],
35
- "sources": [
36
- {
37
- "title": "Unicode Standard Annex #10: Unicode Collation Algorithm",
38
- "url": "https://www.unicode.org/reports/tr10/"
39
- },
40
- {
41
- "title": "MDN: String.prototype.localeCompare()",
42
- "url": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/localeCompare"
43
- }
44
- ],
45
- "tags": [
46
- "sorting",
47
- "comparison",
48
- "names",
49
- "i18n",
50
- "display"
51
- ],
52
- "test_cases": [
53
- {
54
- "inputs": [
55
- "zebra",
56
- "äpple"
57
- ],
58
- "relation": "returns",
59
- "expected": false,
60
- "note": "their_function(a, b) as a human-order \"a sorts before b\" test should return false: äpple belongs before zebra. Raw `a < b` returns true here, which is the trap."
61
- }
62
- ]
63
- }
@@ -1,66 +0,0 @@
1
- {
2
- "id": "byte-order-caps-before-lowercase",
3
- "title": "\"Zebra\" sorts before \"apple\" by default",
4
- "category": "sorting",
5
- "assumption": "Sorting a list of names or words alphabetically puts them in the order a person would expect: apple, then zebra.",
6
- "input": {
7
- "display": "[\"Zebra\", \"apple\", \"Mango\", \"banana\"]",
8
- "description": "A list of user-entered names, tags, or product titles with inconsistent capitalization, sorted with the language's default string comparison for a dropdown or report."
9
- },
10
- "what_goes_wrong": "The default sort returns [\"Mango\", \"Zebra\", \"apple\", \"banana\"] — every capitalized word sorts before every lowercase word, so \"Zebra\" ends up before \"apple\" even though no human alphabetizing this list would ever do that.",
11
- "why": "Python's default string comparison orders characters by Unicode code point and JavaScript's by UTF-16 code unit; for ASCII letters both are the same as byte order. The uppercase letters A-Z (U+0041-U+005A) all come before the lowercase letters a-z (U+0061-U+007A), so a word starting with a capital sorts before every word starting with a lowercase ASCII letter, because the comparison is settled at the first character where the two strings differ and here that is the first letter.",
12
- "affects": [
13
- "python",
14
- "javascript"
15
- ],
16
- "repros": [
17
- {
18
- "language": "python",
19
- "code": "words = [\"Zebra\", \"apple\", \"Mango\", \"banana\"]\nprint(sorted(words))\nprint(\"Zebra\" < \"apple\")\n",
20
- "expected_output": "['Mango', 'Zebra', 'apple', 'banana']\nTrue",
21
- "verified_with": "Python 3.14.2"
22
- },
23
- {
24
- "language": "javascript",
25
- "code": "const words = [\"Zebra\", \"apple\", \"Mango\", \"banana\"];\nconsole.log(words.slice().sort());\nconsole.log(\"Zebra\" < \"apple\");\n",
26
- "expected_output": "[ 'Mango', 'Zebra', 'apple', 'banana' ]\ntrue",
27
- "verified_with": "Node.js v22.22.2"
28
- }
29
- ],
30
- "fix": "Sort case-insensitively for anything a human will read: key on the lowercased (or casefolded) string, or use a locale-aware comparator (Intl.Collator in JS; Python has no case-insensitive built-in comparator, so pass key=str.casefold to sorted()).",
31
- "related": [
32
- "accented-letters-sort-after-z",
33
- "case-insensitive-sort-lower-vs-casefold"
34
- ],
35
- "sources": [
36
- {
37
- "title": "Unicode Standard: Code Charts, Basic Latin block",
38
- "url": "https://www.unicode.org/charts/PDF/U0000.pdf"
39
- },
40
- {
41
- "title": "Python docs: Comparisons (str ordering is by code point)",
42
- "url": "https://docs.python.org/3/reference/expressions.html#comparisons"
43
- },
44
- {
45
- "title": "ECMA-262: Array.prototype.sort, default SortCompare",
46
- "url": "https://tc39.es/ecma262/#sec-array.prototype.sort"
47
- }
48
- ],
49
- "tags": [
50
- "sorting",
51
- "comparison",
52
- "names",
53
- "display"
54
- ],
55
- "test_cases": [
56
- {
57
- "inputs": [
58
- "Zebra",
59
- "apple"
60
- ],
61
- "relation": "returns",
62
- "expected": false,
63
- "note": "their_function(a, b) as a display-order \"a sorts before b\" test should return false: apple belongs before Zebra. Raw `a < b` returns true here, which is the trap."
64
- }
65
- ]
66
- }
@@ -1,56 +0,0 @@
1
- {
2
- "id": "case-insensitive-sort-lower-vs-casefold",
3
- "title": "Switching a sort key from .lower() to .casefold() changes the order of unrelated-looking rows",
4
- "category": "sorting",
5
- "assumption": "lower() and casefold() are interchangeable ways to spell \"case-insensitive sort key\" — swapping one for the other can only change case-sensitivity bugs, never the actual ordering of already-matching rows.",
6
- "input": {
7
- "display": "[\"Stra\\u00dfe\", \"strasse\", \"Strasse\"] sorted with key=str.lower vs key=str.casefold",
8
- "description": "A list of street-name or username variants containing German ß, sorted case-insensitively for display or dedup grouping."
9
- },
10
- "what_goes_wrong": "sorted(words, key=str.lower) returns [\"strasse\", \"Strasse\", \"Stra\\u00dfe\"]: \"Stra\\u00dfe\" sorts last, after both ASCII spellings. sorted(words, key=str.casefold) returns [\"Stra\\u00dfe\", \"strasse\", \"Strasse\"]: \"Stra\\u00dfe\" now sorts FIRST. The three rows are the same; only the key function changed. JavaScript shows the same flip between a toLowerCase() comparator and an Intl.Collator with sensitivity \"base\".",
11
- "why": "str.lower() leaves ß as ß, so the key for \"Straße\" is \"straße\", which differs from \"strasse\" at the fifth character; ß (U+00DF) is a higher code point than \"s\", so it sorts after both ASCII spellings. str.casefold() applies Unicode full case folding (CaseFolding.txt), which maps ß to \"ss\", so the key for \"Straße\" becomes \"strasse\", identical to the other two rows' keys. With all three keys tied, Python's stable sort keeps the rows in input order, which is why \"Straße\" (first in the input) comes out first instead of last. JavaScript has no casefold(), but the same shift happens when a comparator moves from toLowerCase() to an Intl.Collator with sensitivity \"base\", which also treats ß and ss as equal.",
12
- "affects": [
13
- "python",
14
- "javascript"
15
- ],
16
- "repros": [
17
- {
18
- "language": "python",
19
- "code": "words = [\"Stra\\u00dfe\", \"strasse\", \"Strasse\"]\nprint(sorted(words, key=str.lower))\nprint(sorted(words, key=str.casefold))\n",
20
- "expected_output": "['strasse', 'Strasse', 'Straße']\n['Straße', 'strasse', 'Strasse']",
21
- "verified_with": "Python 3.14.2"
22
- },
23
- {
24
- "language": "javascript",
25
- "code": "const words = [\"Stra\\u00dfe\", \"strasse\", \"Strasse\"];\nconst byLower = (a, b) => { const x = a.toLowerCase(), y = b.toLowerCase(); return x < y ? -1 : x > y ? 1 : 0; };\nconsole.log(words.slice().sort(byLower));\nconsole.log(words.slice().sort(new Intl.Collator(\"en\", { sensitivity: \"base\" }).compare));\n",
26
- "expected_output": "[ 'strasse', 'Strasse', 'Straße' ]\n[ 'Straße', 'strasse', 'Strasse' ]",
27
- "verified_with": "Node.js v22.22.2"
28
- }
29
- ],
30
- "fix": "Decide deliberately whether your case-insensitive key should treat folding equivalences like \\u00df\\u2192ss as ties, and know that changing the folding function (lower() vs casefold() in Python, toLowerCase() vs a base-sensitivity Intl.Collator in JavaScript) can change sort order, not just equality. If the final order must be deterministic, add a tiebreaker (for example the original string) after the case-insensitive key: key=lambda s: (s.casefold(), s).",
31
- "related": [
32
- "german-eszett-case-fold",
33
- "byte-order-caps-before-lowercase"
34
- ],
35
- "sources": [
36
- {
37
- "title": "Python docs: str.casefold",
38
- "url": "https://docs.python.org/3/library/stdtypes.html#str.casefold"
39
- },
40
- {
41
- "title": "Unicode Standard: CaseFolding.txt",
42
- "url": "https://www.unicode.org/Public/UCD/latest/ucd/CaseFolding.txt"
43
- },
44
- {
45
- "title": "MDN: Intl.Collator() constructor, sensitivity option",
46
- "url": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Collator/Collator"
47
- }
48
- ],
49
- "tags": [
50
- "sorting",
51
- "comparison",
52
- "names",
53
- "i18n",
54
- "normalization"
55
- ]
56
- }
@@ -1,49 +0,0 @@
1
- {
2
- "id": "comparator-returns-boolean-js",
3
- "title": "arr.sort((a, b) => a > b) can leave an array completely unsorted",
4
- "category": "sorting",
5
- "assumption": "A sort comparator just needs to return something truthy when the first argument should come after the second.",
6
- "input": {
7
- "display": "[5, 3, 8, 1, 9, 2, 7, 4, 6].sort((a, b) => a > b)",
8
- "description": "A comparator written the way a boolean-returning comparison reads naturally in prose (\"is a bigger than b\"), instead of returning a signed number."
9
- },
10
- "what_goes_wrong": "In Node.js (V8), sort((a, b) => a > b) on this 9-element array returns it unchanged, [5, 3, 8, 1, 9, 2, 7, 4, 6], while the numeric comparator sort((a, b) => a - b) returns it fully sorted. No error, no warning.",
11
- "why": "A sort comparator must return a number: negative if a comes first, positive if b comes first, zero if either order is fine. A boolean is converted with ToNumber to 1 or 0, so this comparator can never say \"a comes first\". That makes it inconsistent (it reports 1 and 5 as equal but 5 and 1 as ordered), and for an inconsistent comparator the ECMAScript spec says the resulting order is implementation-defined, so what you get depends on the engine. V8's TimSort (Node.js 11+, Chrome 70+) only moves an element after a negative result; this comparator never returns one, so in V8 the whole input is treated as one already-sorted run and comes back in its original order. Older V8 versions and other engines use different algorithms and may return a partly sorted array instead, which is harder to notice in testing.",
12
- "affects": [
13
- "javascript"
14
- ],
15
- "repros": [
16
- {
17
- "language": "python",
18
- "code": "data = [5, 3, 8, 1, 9, 2, 7, 4, 6]\nprint(sorted(data))\n",
19
- "expected_output": "[1, 2, 3, 4, 5, 6, 7, 8, 9]",
20
- "verified_with": "Python 3.14.2"
21
- },
22
- {
23
- "language": "javascript",
24
- "code": "const data = [5, 3, 8, 1, 9, 2, 7, 4, 6];\nconsole.log(data.slice().sort((a, b) => a > b));\nconsole.log(data.slice().sort((a, b) => a - b));\n",
25
- "expected_output": "[\n 5, 3, 8, 1, 9,\n 2, 7, 4, 6\n]\n[\n 1, 2, 3, 4, 5,\n 6, 7, 8, 9\n]",
26
- "verified_with": "Node.js v22.22.2"
27
- }
28
- ],
29
- "fix": "Always return a signed number from a JavaScript sort comparator (a - b for numbers), never a boolean. Python's sorted()/list.sort() sidestep this entirely — Python 3 removed the old cmp= parameter and only accepts a key= function, so there is no comparator-return-type footgun to have.",
30
- "related": [
31
- "nan-in-sort"
32
- ],
33
- "sources": [
34
- {
35
- "title": "ECMA-262: Array.prototype.sort, comparator requirements",
36
- "url": "https://tc39.es/ecma262/#sec-array.prototype.sort"
37
- },
38
- {
39
- "title": "MDN: Array.prototype.sort() — description of compareFn",
40
- "url": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#description"
41
- }
42
- ],
43
- "tags": [
44
- "sorting",
45
- "comparison",
46
- "pagination",
47
- "display"
48
- ]
49
- }
@@ -1,68 +0,0 @@
1
- {
2
- "id": "js-array-sort-stringifies-numbers",
3
- "title": "[10, 9, 1].sort() returns [1, 10, 9]",
4
- "category": "sorting",
5
- "assumption": "Calling .sort() on an array of numbers sorts them numerically.",
6
- "input": {
7
- "display": "[10, 9, 1].sort()",
8
- "description": "Sorting a plain JavaScript array of numbers (scores, counts, prices) without passing a comparator — the single most common sort call in JavaScript."
9
- },
10
- "what_goes_wrong": "[10, 9, 1].sort() returns [1, 10, 9] — not [1, 9, 10]. 10 sorts before 9.",
11
- "why": "When no comparator is supplied, the ECMAScript spec has Array.prototype.sort() convert each element to a string and compare the strings (undefined values are set aside and placed at the end). It is never numeric unless you pass a comparator. As strings, \"10\" < \"9\" is true, because \"1\" has a lower code unit than \"9\" and string comparison stops at the first differing character, so 10 sorts before 9. Python's sorted() compares the numbers themselves, so this failure does not exist there.",
12
- "affects": [
13
- "javascript"
14
- ],
15
- "repros": [
16
- {
17
- "language": "python",
18
- "code": "print(sorted([10, 9, 1]))\n",
19
- "expected_output": "[1, 9, 10]",
20
- "verified_with": "Python 3.14.2"
21
- },
22
- {
23
- "language": "javascript",
24
- "code": "console.log([10, 9, 1].sort());\nconsole.log([10, 9, 1].sort((a, b) => a - b));\n",
25
- "expected_output": "[ 1, 10, 9 ]\n[ 1, 9, 10 ]",
26
- "verified_with": "Node.js v22.22.2"
27
- }
28
- ],
29
- "fix": "Always pass an explicit numeric comparator to Array.prototype.sort() when sorting numbers: arr.sort((a, b) => a - b). There is no \"numeric mode\" flag — the comparator is the only way to opt out of string comparison.",
30
- "related": [
31
- "natural-sort-breaks-lexicographic",
32
- "typed-array-sort-is-numeric-by-default"
33
- ],
34
- "sources": [
35
- {
36
- "title": "ECMA-262: Array.prototype.sort, default SortCompare behavior",
37
- "url": "https://tc39.es/ecma262/#sec-array.prototype.sort"
38
- },
39
- {
40
- "title": "MDN: Array.prototype.sort()",
41
- "url": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort"
42
- }
43
- ],
44
- "tags": [
45
- "sorting",
46
- "comparison",
47
- "display",
48
- "pagination"
49
- ],
50
- "test_cases": [
51
- {
52
- "inputs": [
53
- [
54
- 10,
55
- 9,
56
- 1
57
- ]
58
- ],
59
- "relation": "returns",
60
- "expected": [
61
- 1,
62
- 9,
63
- 10
64
- ],
65
- "note": "A numeric sort should return [1, 9, 10]. An implementation that calls arr.sort() with no comparator returns [1, 10, 9]."
66
- }
67
- ]
68
- }
@@ -1,54 +0,0 @@
1
- {
2
- "id": "locale-collation-de-vs-sv",
3
- "title": "The same three letters sort in a different order in German than in Swedish",
4
- "category": "sorting",
5
- "assumption": "Locale-aware sorting just fixes the \"accents sort wrong\" problem the same way for every language.",
6
- "input": {
7
- "display": "[\"z\", \"\\u00e4\", \"a\"].sort with localeCompare(..., \"de\") vs localeCompare(..., \"sv\")",
8
- "description": "A single sort function applied to the same three-letter list, parameterized only by a BCP 47 locale tag — e.g. a user-preference-driven \"sort my contacts\" feature."
9
- },
10
- "what_goes_wrong": "localeCompare with locale \"de\" (German) sorts the list as [\"a\", \"\\u00e4\", \"z\"] — ä treated as a minor variant of a, placed right next to it. localeCompare with locale \"sv\" (Swedish) sorts the exact same list as [\"a\", \"z\", \"\\u00e4\"] — ä treated as its own letter, placed after z. There is no single \"correct\" locale-aware order; it depends entirely on which locale you pass.",
11
- "why": "German collation in CLDR (the data behind ICU and JavaScript's Intl) uses the Unicode default ordering with no tailoring: ä is an a with an accent difference, so it sorts right next to a, which matches German dictionary order (DIN 5007-1). CLDR also has a German phonebook variant (de-u-co-phonebk) that sorts ä as \"ae\". Swedish treats å, ä and ö as separate letters at the end of the alphabet, after z, and the Swedish CLDR collation tailors them there. Both are correct for their own language; a collator has to be told which one to use through its locale argument, and a wrong or missing locale applies one language's rules to another's text. Python's sorted() in the repro is plain code-point order, which happens to agree with Swedish here only because ä (U+00E4) is numerically after z.",
12
- "affects": [
13
- "javascript"
14
- ],
15
- "repros": [
16
- {
17
- "language": "python",
18
- "code": "words = [\"z\", \"\\u00e4\", \"a\"]\nprint(sorted(words))\n",
19
- "expected_output": "['a', 'z', 'ä']",
20
- "verified_with": "Python 3.14.2"
21
- },
22
- {
23
- "language": "javascript",
24
- "code": "const words = [\"z\", \"\\u00e4\", \"a\"];\nconsole.log(words.slice().sort((a, b) => a.localeCompare(b, \"de\")));\nconsole.log(words.slice().sort((a, b) => a.localeCompare(b, \"sv\")));\nconsole.log(words.slice().sort());\n",
25
- "expected_output": "[ 'a', 'ä', 'z' ]\n[ 'a', 'z', 'ä' ]\n[ 'a', 'z', 'ä' ]",
26
- "verified_with": "Node.js v22.22.2"
27
- }
28
- ],
29
- "fix": "Always pass the user's actual locale to the collator rather than relying on a default; never assume one European locale's alphabetical order generalizes to another's. JavaScript ships this via Intl.Collator/localeCompare with a locale argument; Python's standard library has no bundled Unicode collation tables, so matching this behavior requires the system's installed locale data (unreliable across machines) or a third-party ICU binding.",
30
- "related": [
31
- "accented-letters-sort-after-z"
32
- ],
33
- "sources": [
34
- {
35
- "title": "Unicode Technical Standard #10: Unicode Collation Algorithm",
36
- "url": "https://www.unicode.org/reports/tr10/"
37
- },
38
- {
39
- "title": "MDN: Intl.Collator",
40
- "url": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Collator"
41
- },
42
- {
43
- "title": "CLDR collation charts",
44
- "url": "https://www.unicode.org/cldr/charts/latest/collation/index.html"
45
- }
46
- ],
47
- "tags": [
48
- "sorting",
49
- "comparison",
50
- "names",
51
- "i18n",
52
- "display"
53
- ]
54
- }