avancement planning

This commit is contained in:
2026-05-26 11:58:39 +02:00
parent 619a2b240a
commit 150b97cd2e
4892 changed files with 99214 additions and 429382 deletions
-21
View File
@@ -1,21 +0,0 @@
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 15
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 7
# Issues with these labels will never be considered stale
exemptLabels:
- "discussion"
- "feature request"
- "bug"
- "help wanted"
- "plugin suggestion"
- "good first issue"
# Label to use when marking an issue as stale
staleLabel: stale
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Thank you
for your contributions.
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: false
-8
View File
@@ -1,8 +0,0 @@
comment: |
Hello! Thank you for contributing!
It appears that you have changed the code, but the tests that verify your change are missing. Could you please add them?
fileExtensions:
- '.ts'
- '.js'
testDir: 'test'
+12 -7
View File
@@ -14,6 +14,11 @@ on:
- 'docs/**'
- '*.md'
# This allows a subsequently queued workflow run to interrupt previous runs
concurrency:
group: "${{ github.workflow }}-${{ github.event.pull_request.head.label || github.head_ref || github.ref }}"
cancel-in-progress: true
permissions:
contents: read
@@ -24,11 +29,11 @@ jobs:
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
persist-credentials: false
- uses: actions/setup-node@v4
- uses: actions/setup-node@v6
with:
node-version: '10'
cache: 'npm'
@@ -66,11 +71,11 @@ jobs:
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
persist-credentials: false
- uses: actions/setup-node@v4
- uses: actions/setup-node@v6
with:
node-version: '24'
cache: 'npm'
@@ -80,21 +85,21 @@ jobs:
- name: Install dependencies
run: |
npm install --ignore-scripts
- if: ${{ matrix.os == 'windows-latest' }}
run: npx playwright install winldd
- name: Run browser tests
run: |
npm run test:browser:${{ matrix.browser }}
test:
needs:
- test-regression-check-node10
permissions:
contents: write
pull-requests: write
uses: fastify/workflows/.github/workflows/plugins-ci.yml@v5
uses: fastify/workflows/.github/workflows/plugins-ci.yml@v6
with:
license-check: true
lint: true
+1 -1
View File
@@ -21,4 +21,4 @@ jobs:
test:
permissions:
contents: read
uses: fastify/workflows/.github/workflows/plugins-ci-package-manager.yml@v5
uses: fastify/workflows/.github/workflows/plugins-ci-package-manager.yml@v6
+1 -3
View File
@@ -1,9 +1,7 @@
Copyright (c) 2011-2021, Gary Court until https://github.com/garycourt/uri-js/commit/a1acf730b4bba3f1097c9f52e7d9d3aba8cdcaae
Copyright (c) 2021-present The Fastify team
Copyright (c) 2021-present The Fastify team <https://github.com/fastify/fastify#team>
All rights reserved.
The Fastify team members are listed at https://github.com/fastify/fastify#team.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
+13 -4
View File
@@ -1,13 +1,9 @@
# fast-uri
<div align="center">
[![NPM version](https://img.shields.io/npm/v/fast-uri.svg?style=flat)](https://www.npmjs.com/package/fast-uri)
[![CI](https://github.com/fastify/fast-uri/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/fastify/fast-uri/actions/workflows/ci.yml)
[![neostandard javascript style](https://img.shields.io/badge/code_style-neostandard-brightgreen?style=flat)](https://github.com/neostandard/neostandard)
</div>
Dependency-free RFC 3986 URI toolbox.
## Usage
@@ -16,6 +12,8 @@ Dependency-free RFC 3986 URI toolbox.
All of the above functions can accept an additional options argument that is an object that can contain one or more of the following properties:
Malformed authorities and out-of-range ports are reported through the parsed component's `error` field. `normalize()` leaves malformed string inputs unchanged, and `equal()` returns `false` when either string input is malformed.
* `scheme` (string)
Indicates the scheme that the URI should be treated as, overriding the URI's normal scheme parsing behavior.
@@ -70,6 +68,17 @@ uri.resolve("uri://a/b/c/d?q", "../../g")
"uri://a/g"
```
### Normalize
```js
const uri = require('fast-uri')
uri.normalize('http://example.com/a%2Fb')
// Output
"http://example.com/a%2Fb"
```
Reserved path escapes such as `%2F` and `%2E` are preserved as path data during normalization and comparison.
### Equal
```js
+90 -24
View File
@@ -1,6 +1,6 @@
'use strict'
const { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizeComponentEncoding, isIPv4, nonSimpleDomain } = require('./lib/utils')
const { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizePercentEncoding, normalizePathEncoding, escapePreservingEscapes, reescapeHostDelimiters, isIPv4, nonSimpleDomain } = require('./lib/utils')
const { SCHEMES, getSchemeHandler } = require('./lib/schemes')
/**
@@ -11,7 +11,7 @@ const { SCHEMES, getSchemeHandler } = require('./lib/schemes')
*/
function normalize (uri, options) {
if (typeof uri === 'string') {
uri = /** @type {T} */ (serialize(parse(uri, options), options))
uri = /** @type {T} */ (normalizeString(uri, options))
} else if (typeof uri === 'object') {
uri = /** @type {T} */ (parse(serialize(uri, options), options))
}
@@ -106,21 +106,10 @@ function resolveComponent (base, relative, options, skipNormalization) {
* @returns {boolean}
*/
function equal (uriA, uriB, options) {
if (typeof uriA === 'string') {
uriA = unescape(uriA)
uriA = serialize(normalizeComponentEncoding(parse(uriA, options), true), { ...options, skipEscape: true })
} else if (typeof uriA === 'object') {
uriA = serialize(normalizeComponentEncoding(uriA, true), { ...options, skipEscape: true })
}
const normalizedA = normalizeComparableURI(uriA, options)
const normalizedB = normalizeComparableURI(uriB, options)
if (typeof uriB === 'string') {
uriB = unescape(uriB)
uriB = serialize(normalizeComponentEncoding(parse(uriB, options), true), { ...options, skipEscape: true })
} else if (typeof uriB === 'object') {
uriB = serialize(normalizeComponentEncoding(uriB, true), { ...options, skipEscape: true })
}
return uriA.toLowerCase() === uriB.toLowerCase()
return normalizedA !== undefined && normalizedB !== undefined && normalizedA.toLowerCase() === normalizedB.toLowerCase()
}
/**
@@ -156,13 +145,13 @@ function serialize (cmpts, opts) {
if (component.path !== undefined) {
if (!options.skipEscape) {
component.path = escape(component.path)
component.path = escapePreservingEscapes(component.path)
if (component.scheme !== undefined) {
component.path = component.path.split('%3A').join(':')
}
} else {
component.path = unescape(component.path)
component.path = normalizePercentEncoding(component.path)
}
}
@@ -213,12 +202,29 @@ function serialize (cmpts, opts) {
const URI_PARSE = /^(?:([^#/:?]+):)?(?:\/\/((?:([^#/?@]*)@)?(\[[^#/?\]]+\]|[^#/:?]*)(?::(\d*))?))?([^#?]*)(?:\?([^#]*))?(?:#((?:.|[\n\r])*))?/u
/**
* @param {import('./types/index').URIComponent} parsed
* @param {RegExpMatchArray} matches
* @returns {string|undefined}
*/
function getParseError (parsed, matches) {
if (matches[2] !== undefined && parsed.path && parsed.path[0] !== '/') {
return 'URI path must start with "/" when authority is present.'
}
if (typeof parsed.port === 'number' && (parsed.port < 0 || parsed.port > 65535)) {
return 'URI port is malformed.'
}
return undefined
}
/**
* @param {string} uri
* @param {import('./types/index').Options} [opts]
* @returns
* @returns {{ parsed: import('./types/index').URIComponent, malformedAuthorityOrPort: boolean }}
*/
function parse (uri, opts) {
function parseWithStatus (uri, opts) {
const options = Object.assign({}, opts)
/** @type {import('./types/index').URIComponent} */
const parsed = {
@@ -231,6 +237,8 @@ function parse (uri, opts) {
fragment: undefined
}
let malformedAuthorityOrPort = false
let isIP = false
if (options.reference === 'suffix') {
if (options.scheme) {
@@ -256,6 +264,13 @@ function parse (uri, opts) {
if (isNaN(parsed.port)) {
parsed.port = matches[5]
}
const parseError = getParseError(parsed, matches)
if (parseError !== undefined) {
parsed.error = parsed.error || parseError
malformedAuthorityOrPort = true
}
if (parsed.host) {
const ipv4result = isIPv4(parsed.host)
if (ipv4result === false) {
@@ -304,14 +319,18 @@ function parse (uri, opts) {
parsed.scheme = unescape(parsed.scheme)
}
if (parsed.host !== undefined) {
parsed.host = unescape(parsed.host)
parsed.host = reescapeHostDelimiters(unescape(parsed.host), isIP)
}
}
if (parsed.path) {
parsed.path = escape(unescape(parsed.path))
parsed.path = normalizePathEncoding(parsed.path)
}
if (parsed.fragment) {
parsed.fragment = encodeURI(decodeURIComponent(parsed.fragment))
try {
parsed.fragment = encodeURI(decodeURIComponent(parsed.fragment))
} catch {
parsed.error = parsed.error || 'URI malformed'
}
}
}
@@ -322,7 +341,54 @@ function parse (uri, opts) {
} else {
parsed.error = parsed.error || 'URI can not be parsed.'
}
return parsed
return { parsed, malformedAuthorityOrPort }
}
/**
* @param {string} uri
* @param {import('./types/index').Options} [opts]
* @returns
*/
function parse (uri, opts) {
return parseWithStatus(uri, opts).parsed
}
/**
* @param {string} uri
* @param {import('./types/index').Options} [opts]
* @returns {string}
*/
function normalizeString (uri, opts) {
return normalizeStringWithStatus(uri, opts).normalized
}
/**
* @param {string} uri
* @param {import('./types/index').Options} [opts]
* @returns {{ normalized: string, malformedAuthorityOrPort: boolean }}
*/
function normalizeStringWithStatus (uri, opts) {
const { parsed, malformedAuthorityOrPort } = parseWithStatus(uri, opts)
return {
normalized: malformedAuthorityOrPort ? uri : serialize(parsed, opts),
malformedAuthorityOrPort
}
}
/**
* @param {import ('./types/index').URIComponent|string} uri
* @param {import('./types/index').Options} [opts]
* @returns {string|undefined}
*/
function normalizeComparableURI (uri, opts) {
if (typeof uri === 'string') {
const { normalized, malformedAuthorityOrPort } = normalizeStringWithStatus(uri, opts)
return malformedAuthorityOrPort ? undefined : normalized
}
if (typeof uri === 'object') {
return serialize(uri, opts)
}
}
const fastUri = {
+129 -22
View File
@@ -6,6 +6,15 @@ const isUUID = RegExp.prototype.test.bind(/^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\d
/** @type {(value: string) => boolean} */
const isIPv4 = RegExp.prototype.test.bind(/^(?:(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)$/u)
/** @type {(value: string) => boolean} */
const isHexPair = RegExp.prototype.test.bind(/^[\da-f]{2}$/iu)
/** @type {(value: string) => boolean} */
const isUnreserved = RegExp.prototype.test.bind(/^[\da-z\-._~]$/iu)
/** @type {(value: string) => boolean} */
const isPathCharacter = RegExp.prototype.test.bind(/^[\da-z\-._~!$&'()*+,;=:@/]$/iu)
/**
* @param {Array<string>} input
* @returns {string}
@@ -264,31 +273,126 @@ function removeDotSegments (path) {
}
/**
* @param {import('../types/index').URIComponent} component
* @param {boolean} esc
* @returns {import('../types/index').URIComponent}
* Re-escape RFC 3986 gen-delims that must not appear literally in the host.
* After the URI regex parses, these characters cannot be literal in the host
* field, so any that appear after decoding came from percent-encoding and
* must be restored to prevent authority structure changes.
*
* @param {string} host
* @param {boolean} isIP - true for IPv4/IPv6 hosts (skip colon re-escaping)
* @returns {string}
*/
function normalizeComponentEncoding (component, esc) {
const func = esc !== true ? escape : unescape
if (component.scheme !== undefined) {
component.scheme = func(component.scheme)
const HOST_DELIMS = { '@': '%40', '/': '%2F', '?': '%3F', '#': '%23', ':': '%3A' }
const HOST_DELIM_RE = /[@/?#:]/g
const HOST_DELIM_NO_COLON_RE = /[@/?#]/g
function reescapeHostDelimiters (host, isIP) {
const re = isIP ? HOST_DELIM_NO_COLON_RE : HOST_DELIM_RE
re.lastIndex = 0
return host.replace(re, (ch) => HOST_DELIMS[ch])
}
/**
* Normalizes percent escapes and optionally decodes only unreserved ASCII bytes.
* Reserved delimiters such as `%2F` and `%2E` stay escaped.
*
* @param {string} input
* @param {boolean} [decodeUnreserved=false]
* @returns {string}
*/
function normalizePercentEncoding (input, decodeUnreserved = false) {
if (input.indexOf('%') === -1) {
return input
}
if (component.userinfo !== undefined) {
component.userinfo = func(component.userinfo)
let output = ''
for (let i = 0; i < input.length; i++) {
if (input[i] === '%' && i + 2 < input.length) {
const hex = input.slice(i + 1, i + 3)
if (isHexPair(hex)) {
const normalizedHex = hex.toUpperCase()
const decoded = String.fromCharCode(parseInt(normalizedHex, 16))
if (decodeUnreserved && isUnreserved(decoded)) {
output += decoded
} else {
output += '%' + normalizedHex
}
i += 2
continue
}
}
output += input[i]
}
if (component.host !== undefined) {
component.host = func(component.host)
return output
}
/**
* Normalizes path data without turning reserved escapes into live path syntax.
* Valid escapes are uppercased, raw unsafe characters are escaped, and only
* unreserved bytes that are not `.` are decoded.
*
* @param {string} input
* @returns {string}
*/
function normalizePathEncoding (input) {
let output = ''
for (let i = 0; i < input.length; i++) {
if (input[i] === '%' && i + 2 < input.length) {
const hex = input.slice(i + 1, i + 3)
if (isHexPair(hex)) {
const normalizedHex = hex.toUpperCase()
const decoded = String.fromCharCode(parseInt(normalizedHex, 16))
if (decoded !== '.' && isUnreserved(decoded)) {
output += decoded
} else {
output += '%' + normalizedHex
}
i += 2
continue
}
}
if (isPathCharacter(input[i])) {
output += input[i]
} else {
output += escape(input[i])
}
}
if (component.path !== undefined) {
component.path = func(component.path)
return output
}
/**
* Escapes a component while preserving existing valid percent escapes.
*
* @param {string} input
* @returns {string}
*/
function escapePreservingEscapes (input) {
let output = ''
for (let i = 0; i < input.length; i++) {
if (input[i] === '%' && i + 2 < input.length) {
const hex = input.slice(i + 1, i + 3)
if (isHexPair(hex)) {
output += '%' + hex.toUpperCase()
i += 2
continue
}
}
output += escape(input[i])
}
if (component.query !== undefined) {
component.query = func(component.query)
}
if (component.fragment !== undefined) {
component.fragment = func(component.fragment)
}
return component
return output
}
/**
@@ -310,7 +414,7 @@ function recomposeAuthority (component) {
if (ipV6res.isIPV6 === true) {
host = `[${ipV6res.escapedHost}]`
} else {
host = component.host
host = reescapeHostDelimiters(host, false)
}
}
uriTokens.push(host)
@@ -327,7 +431,10 @@ function recomposeAuthority (component) {
module.exports = {
nonSimpleDomain,
recomposeAuthority,
normalizeComponentEncoding,
reescapeHostDelimiters,
normalizePercentEncoding,
normalizePathEncoding,
escapePreservingEscapes,
removeDotSegments,
isIPv4,
isUUID,
+3 -4
View File
@@ -1,7 +1,7 @@
{
"name": "fast-uri",
"description": "Dependency-free RFC 3986 URI toolbox",
"version": "3.1.0",
"version": "3.1.2",
"main": "index.js",
"type": "commonjs",
"types": "types/index.d.ts",
@@ -58,12 +58,11 @@
"test:typescript": "tsd"
},
"devDependencies": {
"@fastify/pre-commit": "^2.1.0",
"ajv": "^8.16.0",
"eslint": "^9.17.0",
"neostandard": "^0.12.0",
"neostandard": "^0.13.0",
"playwright-test": "^14.1.12",
"tape": "^5.8.1",
"tsd": "^0.32.0"
"tsd": "^0.33.0"
}
}
+9
View File
@@ -106,3 +106,12 @@ test('WSS Equal', (t) => {
runTest(t, suite)
t.end()
})
test('URI Equals tolerates malformed fragments', (t) => {
t.equal(
fastURI.equal('http://example.com/#%E0%A4A', 'http://example.com/#%E0%A4A'),
true,
'malformed fragment does not throw during equality checks'
)
t.end()
})
+5
View File
@@ -150,6 +150,11 @@ test('URI parse', (t) => {
t.equal(components.query, undefined, 'query')
t.equal(components.fragment, '%0D', 'fragment')
// malformed percent-encoded fragment must not throw
components = fastURI.parse('http://example.com/#%E0%A4A')
t.equal(components.error, 'URI malformed', 'malformed fragment errors')
t.equal(components.fragment, '%E0%A4A', 'malformed fragment is preserved')
// all
components = fastURI.parse('uri://user:pass@example.com:123/one/two.three?q1=a1&q2=a2#body')
t.equal(components.error, undefined, 'all errors')
+9
View File
@@ -76,3 +76,12 @@ test('URN Resolving', (t) => {
t.equal(fastURI.resolve('urn:some:other:prop', 'urn:some:ip:prop'), 'urn:some:ip:prop', 'urn:some:ip:prop')
t.end()
})
test('URI Resolving tolerates malformed fragments', (t) => {
t.equal(
fastURI.resolve('http://base.com/', 'http://example.com/#%E0%A4A'),
'http://example.com/#%E0%A4A',
'malformed fragment does not throw during resolve'
)
t.end()
})