Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions api/docs/tough-cookie.defaultpath.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@

## defaultPath() function

> Warning: This API is now obsolete.
>
> This function will be removed in a future version of tough-cookie. If you rely on this function, please open an issue to discuss why it should remain public.
>

Given a current request/response path, gives the path appropriate for storing in a cookie. This is basically the "directory" of a "file" in the path, but is specified by [RFC6265 - Section 5.1.4](https://www.rfc-editor.org/rfc/rfc6265.html#section-5.1.4)<!-- -->.

**Signature:**
Expand Down
5 changes: 5 additions & 0 deletions api/docs/tough-cookie.pathmatch.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@

## pathMatch() function

> Warning: This API is now obsolete.
>
> This function will be removed in a future version of tough-cookie. If you rely on this function, please open an issue to discuss why it should remain public.
>

Answers "does the request-path path-match a given cookie-path?" as per [RFC6265 Section 5.1.4](https://www.rfc-editor.org/rfc/rfc6265.html#section-5.1.4)<!-- -->. This is essentially a prefix-match where cookiePath is a prefix of reqPath.

**Signature:**
Expand Down
5 changes: 5 additions & 0 deletions api/docs/tough-cookie.permutepath.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@

## permutePath() function

> Warning: This API is now obsolete.
>
> This function will be removed in a future version of tough-cookie. If you rely on this function, please open an issue to discuss why it should remain public.
>

Generates the permutation of all possible values that [pathMatch()](./tough-cookie.pathmatch.md) the `path` parameter. The array is in longest-to-shortest order. Useful when building custom [Store](./tough-cookie.store.md) implementations.

**Signature:**
Expand Down
6 changes: 3 additions & 3 deletions api/tough-cookie.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ export interface CreateCookieOptions {
value?: string;
}

// @public
// @public @deprecated
export function defaultPath(path?: Nullable<string>): string;

// @public
Expand Down Expand Up @@ -236,13 +236,13 @@ export interface ParseCookieOptions {
// @public
export function parseDate(cookieDate: Nullable<string>): Date | undefined;

// @public
// @public @deprecated
export function pathMatch(reqPath: string, cookiePath: string): boolean;

// @public
export function permuteDomain(domain: string, allowSpecialUseDomain?: boolean): string[] | undefined;

// @public
// @public @deprecated
export function permutePath(path: string): string[];

// @public
Expand Down
30 changes: 7 additions & 23 deletions lib/__tests__/defaultPath.spec.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,13 @@
/* eslint-disable @typescript-eslint/no-deprecated */
import { describe, expect, it } from 'vitest'
import { defaultPath } from '../cookie/defaultPath.js'
import { defaultPathCases } from './data/defaultPathCases.js'

describe('defaultPath', () => {
it.each([
{
input: null,
output: '/',
it.each(defaultPathCases)(
'defaultPath("$input") => $expected',
({ input, expected }) => {
expect(defaultPath(input)).toBe(expected)
},
{
input: '/',
output: '/',
},
{
input: '/file',
output: '/',
},
{
input: '/dir/file',
output: '/dir',
},
{
input: 'noslash',
output: '/',
},
])('defaultPath("$input") => $output', ({ input, output }) => {
expect(defaultPath(input)).toBe(output)
})
)
})
49 changes: 39 additions & 10 deletions lib/__tests__/pathMatch.spec.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,49 @@
/* eslint-disable @typescript-eslint/no-deprecated */
import { describe, expect, it } from 'vitest'
import { pathMatch } from '../pathMatch.js'
import { pathMatchCases } from './data/pathMatchCases.js'

describe('pathMatch', () => {
it.each([
// request, cookie, match
['/', '/', true],
['/dir', '/', true],
['/', '/dir', false],
['/dir/', '/dir/', true],
['/dir/file', '/dir/', true],
['/dir/file', '/dir', true],
['/directory', '/dir', false],
])(
it.each(pathMatchCases)(
'pathMatch("%s", "%s") => %s',
(requestPath, cookiePath, expectedValue) => {
expect(pathMatch(requestPath, cookiePath)).toBe(expectedValue)
},
)

// LEGACY BEHAVIOR — DO NOT EXTEND.
// These cases pin pre-deprecation behavior for non-RFC-compliant inputs
// (paths not starting with "/"). RFC 6265 cookie-paths always start with "/",
// so these inputs are out of spec, but earlier versions still prefix-matched
// them and direct callers may rely on it. pathMatch is deprecated; once it is
// removed these tests should be deleted along with the fallback they cover.
describe('non-RFC-compliant input fallback (legacy, pending removal)', () => {
it('returns true when both invalid inputs are equal', () => {
expect(pathMatch('foo', 'foo')).toBe(true)
})

it('returns false when invalid inputs differ', () => {
expect(pathMatch('foo', 'bar')).toBe(false)
})

it('returns false when only reqPath is invalid', () => {
expect(pathMatch('foo', '/bar')).toBe(false)
})

it('returns false when only cookiePath is invalid', () => {
expect(pathMatch('/foo', 'bar')).toBe(false)
})

it('matches when the invalid cookiePath is a prefix ending in "/"', () => {
expect(pathMatch('foo/bar', 'foo/')).toBe(true)
})

it('matches when the invalid cookiePath is a prefix at a "/" boundary', () => {
expect(pathMatch('foo/bar', 'foo')).toBe(true)
})

it('does not match when the invalid prefix is not at a "/" boundary', () => {
expect(pathMatch('foobar', 'foo')).toBe(false)
})
})
})
48 changes: 27 additions & 21 deletions lib/__tests__/permutePath.spec.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,35 @@
/* eslint-disable @typescript-eslint/no-deprecated */
import { describe, expect, it } from 'vitest'
import { permutePath } from '../cookie/permutePath.js'
import { pathMatch } from '../pathMatch.js'
import { permutePathCases } from './data/permutePathCases.js'

describe('permutePath', () => {
it.each([
{
path: '/',
permutations: ['/'],
it.each(permutePathCases)(
'permutePath("$path") => $permutations',
({ path, permutations }) => {
expect(permutePath(path)).toEqual([...permutations])
permutations.forEach((permutation) => {
expect(pathMatch(path, permutation)).toBe(true)
})
},
{
path: '/foo',
permutations: ['/foo', '/'],
},
{
path: '/foo/bar',
permutations: ['/foo/bar', '/foo', '/'],
},
{
path: '/foo/bar/',
permutations: ['/foo/bar/', '/foo/bar', '/foo', '/'],
},
])('permuteDomain("%s", %s") => %o', ({ path, permutations }) => {
expect(permutePath(path)).toEqual(permutations)
permutations.forEach((permutation) => {
expect(pathMatch(path, permutation)).toBe(true)
})
)

// LEGACY BEHAVIOR — DO NOT EXTEND.
// RFC 6265 cookie-paths always start with "/", so a path without a leading
// "/" is out of spec. Earlier versions still peeled it down character by
// character, and direct callers may rely on it. permutePath is deprecated;
// once it is removed this test should be deleted along with the fallback.
it('preserves legacy behavior for non-RFC-compliant input (pending removal)', () => {
expect(permutePath('noslash')).toEqual([
'noslash',
'noslas',
'nosla',
'nosl',
'nos',
'no',
'n',
'/',
])
})
})
8 changes: 7 additions & 1 deletion lib/cookie/cookieJar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -613,6 +613,7 @@ export class CookieJar {
//attribute-value is not %x2F ("/"):
//Let cookie-path be the default-path.
if (!cookie.path || cookie.path[0] !== '/') {
// eslint-disable-next-line @typescript-eslint/no-deprecated -- migrated to CookiePath in a follow-up
cookie.path = defaultPath(context.pathname)
cookie.pathIsDefault = true
}
Expand Down Expand Up @@ -929,7 +930,12 @@ export class CookieJar {
}

// "The request-uri's path path-matches the cookie's path."
if (!allPaths && typeof c.path === 'string' && !pathMatch(path, c.path)) {
if (
!allPaths &&
typeof c.path === 'string' &&
// eslint-disable-next-line @typescript-eslint/no-deprecated -- migrated to CookiePath in a follow-up
!pathMatch(path, c.path)
) {
return false
}

Expand Down
23 changes: 3 additions & 20 deletions lib/cookie/defaultPath.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Nullable } from '../utils.js'
import * as CookiePath from './cookiePath.js'

/**
* Given a current request/response path, gives the path appropriate for storing
Expand Down Expand Up @@ -36,27 +37,9 @@ import type { Nullable } from '../utils.js'
* ```
*
* @param path - the path portion of the request-uri (excluding the hostname, query, fragment, and so on)
* @deprecated This function will be removed in a future version of tough-cookie. If you rely on this function, please open an issue to discuss why it should remain public.
* @public
*/
export function defaultPath(path?: Nullable<string>): string {
// "2. If the uri-path is empty or if the first character of the uri-path is not
// a %x2F ("/") character, output %x2F ("/") and skip the remaining steps.
if (!path || path.slice(0, 1) !== '/') {
return '/'
}

// "3. If the uri-path contains no more than one %x2F ("/") character, output
// %x2F ("/") and skip the remaining step."
if (path === '/') {
return path
}

const rightSlash = path.lastIndexOf('/')
if (rightSlash === 0) {
return '/'
}

// "4. Output the characters of the uri-path from the first character up to,
// but not including, the right-most %x2F ("/")."
return path.slice(0, rightSlash)
return CookiePath.defaultPath(path)
}
3 changes: 3 additions & 0 deletions lib/cookie/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export { MemoryCookieStore, type MemoryCookieStoreIndex } from '../memstore.js'
// eslint-disable-next-line @typescript-eslint/no-deprecated -- re-exported for backward compatibility
export { pathMatch } from '../pathMatch.js'
export { permuteDomain } from '../permuteDomain.js'
export {
Expand Down Expand Up @@ -27,10 +28,12 @@ export {
type GetCookiesOptions,
type SetCookieOptions,
} from './cookieJar.js'
// eslint-disable-next-line @typescript-eslint/no-deprecated -- re-exported for backward compatibility
export { defaultPath } from './defaultPath.js'
export { domainMatch } from './domainMatch.js'
export { formatDate } from './formatDate.js'
export { parseDate } from './parseDate.js'
// eslint-disable-next-line @typescript-eslint/no-deprecated -- re-exported for backward compatibility
export { permutePath } from './permutePath.js'

import { Cookie, ParseCookieOptions } from './cookie.js'
Expand Down
22 changes: 16 additions & 6 deletions lib/cookie/permutePath.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import * as CookiePath from './cookiePath.js'

/**
* Generates the permutation of all possible values that {@link pathMatch} the `path` parameter.
* The array is in longest-to-shortest order. Useful when building custom {@link Store} implementations.
Expand All @@ -9,20 +11,28 @@
* ```
*
* @param path - the path to generate permutations for
* @deprecated This function will be removed in a future version of tough-cookie. If you rely on this function, please open an issue to discuss why it should remain public.
* @public
*/
export function permutePath(path: string): string[] {
if (path === '/') {
return ['/']
const parsed = CookiePath.parse(path)
if (parsed) {
return CookiePath.permute(parsed)
}

// Inputs that are not valid cookie-paths (do not start with "/") are rejected
// by CookiePath.parse. Such inputs are out of spec, but earlier versions of
// this function still produced permutations for them. Fall back to the
// original algorithm to preserve that behavior for direct callers.
const permutations = [path]
while (path.length > 1) {
const lindex = path.lastIndexOf('/')
let current = path
while (current.length > 1) {
const lindex = current.lastIndexOf('/')
if (lindex === 0) {
break
}
path = path.slice(0, lindex)
permutations.push(path)
current = current.slice(0, lindex)
permutations.push(current)
}
permutations.push('/')
return permutations
Expand Down
1 change: 1 addition & 0 deletions lib/memstore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ export class MemoryCookieStore extends Store {
//NOTE: we should use path-match algorithm from S5.1.4 here
//(see : https://github.com/ChromiumWebApps/chromium/blob/b3d3b4da8bb94c1b2e061600df106d590fda3620/net/cookies/canonical_cookie.cc#L299)
for (const cookiePath in domainIndex) {
// eslint-disable-next-line @typescript-eslint/no-deprecated -- migrated to CookiePath in a follow-up
if (pathMatch(path, cookiePath)) {
const pathIndex = domainIndex[cookiePath]
for (const key in pathIndex) {
Expand Down
25 changes: 15 additions & 10 deletions lib/pathMatch.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import * as CookiePath from './cookie/cookiePath.js'

/**
* Answers "does the request-path path-match a given cookie-path?" as per {@link https://www.rfc-editor.org/rfc/rfc6265.html#section-5.1.4 | RFC6265 Section 5.1.4}.
* This is essentially a prefix-match where cookiePath is a prefix of reqPath.
Expand All @@ -12,26 +14,29 @@
*
* @param reqPath - the path of the request
* @param cookiePath - the path of the cookie
* @deprecated This function will be removed in a future version of tough-cookie. If you rely on this function, please open an issue to discuss why it should remain public.
* @public
*/
export function pathMatch(reqPath: string, cookiePath: string): boolean {
// "o The cookie-path and the request-path are identical."
const parsedReqPath = CookiePath.parse(reqPath)
const parsedCookiePath = CookiePath.parse(cookiePath)
if (parsedReqPath && parsedCookiePath) {
return CookiePath.match(parsedReqPath, parsedCookiePath)
}

// Inputs that are not valid cookie-paths (do not start with "/") are rejected
// by CookiePath.parse. Such inputs are out of spec, but earlier versions of
// this function still matched them via prefix logic. Fall back to the
// original algorithm to preserve that behavior for direct callers.
if (cookiePath === reqPath) {
return true
}

const idx = reqPath.indexOf(cookiePath)
if (idx === 0) {
// "o The cookie-path is a prefix of the request-path, and the last
// character of the cookie-path is %x2F ("/")."
if (reqPath.indexOf(cookiePath) === 0) {
if (cookiePath[cookiePath.length - 1] === '/') {
return true
}

// " o The cookie-path is a prefix of the request-path, and the first
// character of the request-path that is not included in the cookie- path
// is a %x2F ("/") character."
if (reqPath.startsWith(cookiePath) && reqPath[cookiePath.length] === '/') {
if (reqPath[cookiePath.length] === '/') {
return true
}
}
Expand Down
Loading