diff --git a/deno_dist/utils/filepath.ts b/deno_dist/utils/filepath.ts index 35c6159f7..efcc72b6d 100644 --- a/deno_dist/utils/filepath.ts +++ b/deno_dist/utils/filepath.ts @@ -1,51 +1,59 @@ +const DEFAULT_DOCUMENT = 'index.html' +const VALID_EXTENSION_REGEX = /\.[a-zA-Z0-9]+$/ +const PARENT_DIRECTORY_REGEX = /(?:^|[\/\\])\.\.(?:$|[\/\\])/ + type FilePathOptions = { filename: string root?: string defaultDocument?: string } -export const getFilePath = (options: FilePathOptions): string | undefined => { - let filename = options.filename - const defaultDocument = options.defaultDocument || 'index.html' - - if (filename.endsWith('/')) { - // /top/ => /top/index.html - filename = filename.concat(defaultDocument) - } else if (!filename.match(/\.[a-zA-Z0-9]+$/)) { - // /top => /top/index.html - filename = filename.concat('/' + defaultDocument) - } - - const path = getFilePathWithoutDefaultDocument({ - root: options.root, - filename, +/** + * Retrieves the file path based on the provided options. + * If no default document is specified, 'index.html' is appended to the path. + * If the filename ends with '/', the default document is appended. + * If the filename has no valid extension, the default document is appended. + * @param options - The options object containing filename, root, and defaultDocument. + * @returns The final file path or undefined if parent directory traversal is detected. + */ +export const getFilePath = ({ + filename, + defaultDocument, + root, +}: FilePathOptions): string | undefined => { + const isDefaultDocument = defaultDocument || DEFAULT_DOCUMENT + + const shouldAppendDefault = filename.endsWith('/') || !filename.match(VALID_EXTENSION_REGEX) + + const finalFilename = shouldAppendDefault + ? filename.concat('/', isDefaultDocument || DEFAULT_DOCUMENT) + : filename + + return getFilePathWithoutDefaultDocument({ + root, + filename: finalFilename, }) - - return path } -export const getFilePathWithoutDefaultDocument = ( - options: Omit -) => { - let root = options.root || '' - let filename = options.filename - - if (/(?:^|[\/\\])\.\.(?:$|[\/\\])/.test(filename)) { +/** + * Retrieves the file path without appending the default document. + * If parent directory traversal is detected in the filename, returns undefined. + * Replaces backslashes with forward slashes and removes redundant slashes. + * @param options The options object containing filename and root. + * @returns The sanitized file path. + */ +export const getFilePathWithoutDefaultDocument = ({ + filename, + root = '', +}: Omit) => { + if (PARENT_DIRECTORY_REGEX.test(filename)) { return } - // /foo.html => foo.html - filename = filename.replace(/^\.?[\/\\]/, '') - - // foo\bar.txt => foo/bar.txt - filename = filename.replace(/\\/, '/') - - // assets/ => assets - root = root.replace(/\/$/, '') + const sanitizedFilename = filename.replace(/^\.?[\/\\]/, '').replace(/\\/, '/') + const sanitizedRoot = root.replace(/\/$/, '') - // ./assets/foo.html => assets/foo.html - let path = root ? root + '/' + filename : filename - path = path.replace(/^\.?\//, '') + const path = sanitizedRoot ? sanitizedRoot + '/' + sanitizedFilename : sanitizedFilename - return path + return path.replace(/^\.?\//, '') } diff --git a/src/utils/filepath.test.ts b/src/utils/filepath.test.ts index 5d6482e2d..44e3a1d2a 100644 --- a/src/utils/filepath.test.ts +++ b/src/utils/filepath.test.ts @@ -1,7 +1,7 @@ import { getFilePath, getFilePathWithoutDefaultDocument } from './filepath' describe('getFilePathWithoutDefaultDocument', () => { - it('Should return file path correctly', async () => { + it('should return file path correctly without default document', () => { expect(getFilePathWithoutDefaultDocument({ filename: 'foo.txt' })).toBe('foo.txt') expect(getFilePathWithoutDefaultDocument({ filename: 'foo.txt', root: 'bar' })).toBe( 'bar/foo.txt' @@ -10,6 +10,7 @@ describe('getFilePathWithoutDefaultDocument', () => { expect(getFilePathWithoutDefaultDocument({ filename: '../foo' })).toBeUndefined() expect(getFilePathWithoutDefaultDocument({ filename: '/../foo' })).toBeUndefined() expect(getFilePathWithoutDefaultDocument({ filename: './../foo' })).toBeUndefined() + expect(getFilePathWithoutDefaultDocument({ filename: 'foo..bar.txt' })).toBe('foo..bar.txt') expect(getFilePathWithoutDefaultDocument({ filename: '/foo..bar.txt' })).toBe('foo..bar.txt') expect(getFilePathWithoutDefaultDocument({ filename: './foo..bar.txt' })).toBe('foo..bar.txt') @@ -47,9 +48,8 @@ describe('getFilePathWithoutDefaultDocument', () => { }) describe('getFilePath', () => { - it('Should return file path correctly', async () => { + it('should return file path correctly with default document', () => { expect(getFilePath({ filename: 'foo' })).toBe('foo/index.html') - expect(getFilePath({ filename: 'foo', root: 'bar' })).toBe('bar/foo/index.html') expect(getFilePath({ filename: 'foo', defaultDocument: 'index.txt' })).toBe('foo/index.txt') diff --git a/src/utils/filepath.ts b/src/utils/filepath.ts index 35c6159f7..efcc72b6d 100644 --- a/src/utils/filepath.ts +++ b/src/utils/filepath.ts @@ -1,51 +1,59 @@ +const DEFAULT_DOCUMENT = 'index.html' +const VALID_EXTENSION_REGEX = /\.[a-zA-Z0-9]+$/ +const PARENT_DIRECTORY_REGEX = /(?:^|[\/\\])\.\.(?:$|[\/\\])/ + type FilePathOptions = { filename: string root?: string defaultDocument?: string } -export const getFilePath = (options: FilePathOptions): string | undefined => { - let filename = options.filename - const defaultDocument = options.defaultDocument || 'index.html' - - if (filename.endsWith('/')) { - // /top/ => /top/index.html - filename = filename.concat(defaultDocument) - } else if (!filename.match(/\.[a-zA-Z0-9]+$/)) { - // /top => /top/index.html - filename = filename.concat('/' + defaultDocument) - } - - const path = getFilePathWithoutDefaultDocument({ - root: options.root, - filename, +/** + * Retrieves the file path based on the provided options. + * If no default document is specified, 'index.html' is appended to the path. + * If the filename ends with '/', the default document is appended. + * If the filename has no valid extension, the default document is appended. + * @param options - The options object containing filename, root, and defaultDocument. + * @returns The final file path or undefined if parent directory traversal is detected. + */ +export const getFilePath = ({ + filename, + defaultDocument, + root, +}: FilePathOptions): string | undefined => { + const isDefaultDocument = defaultDocument || DEFAULT_DOCUMENT + + const shouldAppendDefault = filename.endsWith('/') || !filename.match(VALID_EXTENSION_REGEX) + + const finalFilename = shouldAppendDefault + ? filename.concat('/', isDefaultDocument || DEFAULT_DOCUMENT) + : filename + + return getFilePathWithoutDefaultDocument({ + root, + filename: finalFilename, }) - - return path } -export const getFilePathWithoutDefaultDocument = ( - options: Omit -) => { - let root = options.root || '' - let filename = options.filename - - if (/(?:^|[\/\\])\.\.(?:$|[\/\\])/.test(filename)) { +/** + * Retrieves the file path without appending the default document. + * If parent directory traversal is detected in the filename, returns undefined. + * Replaces backslashes with forward slashes and removes redundant slashes. + * @param options The options object containing filename and root. + * @returns The sanitized file path. + */ +export const getFilePathWithoutDefaultDocument = ({ + filename, + root = '', +}: Omit) => { + if (PARENT_DIRECTORY_REGEX.test(filename)) { return } - // /foo.html => foo.html - filename = filename.replace(/^\.?[\/\\]/, '') - - // foo\bar.txt => foo/bar.txt - filename = filename.replace(/\\/, '/') - - // assets/ => assets - root = root.replace(/\/$/, '') + const sanitizedFilename = filename.replace(/^\.?[\/\\]/, '').replace(/\\/, '/') + const sanitizedRoot = root.replace(/\/$/, '') - // ./assets/foo.html => assets/foo.html - let path = root ? root + '/' + filename : filename - path = path.replace(/^\.?\//, '') + const path = sanitizedRoot ? sanitizedRoot + '/' + sanitizedFilename : sanitizedFilename - return path + return path.replace(/^\.?\//, '') }