Skip to content

Commit 9b58942

Browse files
authored
Merge pull request #861 from nextcloud-libraries/feat/normalize
feat(normalize): add `normalize` method compatible with the version from `node:path`
2 parents 649d624 + a94aefc commit 9b58942

2 files changed

Lines changed: 62 additions & 0 deletions

File tree

lib/index.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,3 +131,36 @@ export function isSamePath(path1: string, path2: string): boolean {
131131

132132
return path1 === path2
133133
}
134+
135+
/**
136+
* Normalizes the given path by removing leading, trailing and doubled slashes and also removing the dot sections.
137+
*
138+
* @param path - The path to normalize
139+
*/
140+
export function normalize(path: string): string {
141+
const sections = path.split('/')
142+
.filter((p, index, arr) => p !== '' || index === 0 || index === (arr.length - 1)) // remove double // but keep leading and trailing slash
143+
.filter((p) => p !== '.') // remove useless /./ sections
144+
145+
const sanitizedSections: string[] = []
146+
for (const section of sections) {
147+
const lastSection = sanitizedSections.at(-1)
148+
if (section === '..' && lastSection !== '..') {
149+
// if the current section is ".." for the parent, we remove the last section
150+
// But only if the last section is not also ".." which means that we are in a relative path outside of the root
151+
// Note that absolute paths like "/../../foo" are valid as they resolve to "/foo"
152+
if (lastSection === undefined) {
153+
// if there is no last section, we are at the root and we can't go up further
154+
// so we keep the ".." section as this is a relative path outside of the root
155+
sanitizedSections.push(section)
156+
} else if (lastSection !== '') {
157+
// only remove parent if its not the root (leading slash)
158+
sanitizedSections.pop()
159+
}
160+
} else {
161+
sanitizedSections.push(section)
162+
}
163+
}
164+
165+
return sanitizedSections.join('/')
166+
}

test/normalize.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: GPL-3.0-or-later
4+
*/
5+
6+
import { expect, test } from 'vitest'
7+
import { normalize } from '../lib/index.ts'
8+
9+
test('normalize', () => {
10+
expect(normalize('./fixtures///b/../b/c.js')).toBe('fixtures/b/c.js')
11+
expect(normalize('/foo/../../../bar')).toBe('/bar')
12+
expect(normalize('a//b//../b')).toBe('a/b')
13+
expect(normalize('a//b//./c')).toBe('a/b/c')
14+
expect(normalize('a//b//.')).toBe('a/b')
15+
expect(normalize('/a/b/c/../../../x/y/z')).toBe('/x/y/z')
16+
expect(normalize('///..//./foo/.//bar')).toBe('/foo/bar')
17+
expect(normalize('bar/foo../../')).toBe('bar/')
18+
expect(normalize('bar/foo../..')).toBe('bar')
19+
expect(normalize('bar/foo../../baz')).toBe('bar/baz')
20+
expect(normalize('bar/foo../')).toBe('bar/foo../')
21+
expect(normalize('bar/foo..')).toBe('bar/foo..')
22+
expect(normalize('../foo../../../bar')).toBe('../../bar')
23+
expect(normalize('../../.././../../../bar')).toBe('../../../../../../bar')
24+
expect(normalize('../../../foo/../../../bar')).toBe('../../../../../bar')
25+
expect(normalize('../../../foo/../../../bar/../../')).toBe('../../../../../../')
26+
expect(normalize('../foobar/barfoo/foo/../../../bar/../../')).toBe('../../')
27+
expect(normalize('../../../foobar/../../../bar/../../baz')).toBe('../../../../../../baz')
28+
expect(normalize('/../../../foobar/../../../bar/../../baz')).toBe('/baz')
29+
})

0 commit comments

Comments
 (0)