fix(ui-primitives): look up file types in Maps so prototype keys miss
classifyFileType and classifyCodeFileType indexed plain objects by the lowercased basename and extension, so a file named `constructor` or `__proto__` (or with that extension) resolved an inherited Object.prototype member and FileTypeIcon threw from its assertNever, crashing every view that lists the file. The name and extension tables are now Maps, so such names fall through to their real type or `other`. Fixes #8205
This commit is contained in:
1 parent
0eed137eca
commit
7d2d22aaf9
3 files changed
+41
-12
No files matched your search
@@ -44,7 +44,9 @@ export type FileTypeIconProps = IconProps & (
|
||||
}
|
||||
)
|
||||
|
||||
const EXTENSION_TYPES: Readonly<Record<string, ClassifiedFileType>> = {
|
||||
// Maps, not objects: a basename or extension equal to an `Object.prototype` key
|
||||
// (`constructor`, `__proto__`) must miss instead of resolving the inherited member.
|
||||
const EXTENSION_TYPES: ReadonlyMap<string, ClassifiedFileType> = new Map(Object.entries({
|
||||
scss: 'code',
|
||||
sass: 'code',
|
||||
less: 'code',
|
||||
@@ -101,13 +103,13 @@ const EXTENSION_TYPES: Readonly<Record<string, ClassifiedFileType>> = {
|
||||
ots: 'excel',
|
||||
fods: 'excel',
|
||||
numbers: 'excel',
|
||||
}
|
||||
}))
|
||||
|
||||
const NAME_TYPES: Readonly<Record<string, ClassifiedFileType>> = {
|
||||
const NAME_TYPES: ReadonlyMap<string, ClassifiedFileType> = new Map(Object.entries({
|
||||
changelog: 'markdown',
|
||||
contributing: 'markdown',
|
||||
readme: 'markdown',
|
||||
}
|
||||
}))
|
||||
|
||||
function basename(path: string): string {
|
||||
return path.slice(Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\')) + 1)
|
||||
@@ -140,8 +142,8 @@ export function classifyFileType(
|
||||
const name = basename(path).toLowerCase()
|
||||
const extension = fileExtension(name).toLowerCase()
|
||||
return classifyCodeFileType(name, extension, context)
|
||||
?? NAME_TYPES[name]
|
||||
?? EXTENSION_TYPES[extension]
|
||||
?? NAME_TYPES.get(name)
|
||||
?? EXTENSION_TYPES.get(extension)
|
||||
?? 'other'
|
||||
}
|
||||
|
||||
|
||||
@@ -61,7 +61,9 @@ export interface FileTypeProjectContext {
|
||||
|
||||
const CODE_FILE_TYPE_SET = new Set<string>(CODE_FILE_TYPES)
|
||||
|
||||
const FILE_NAME_TYPES: Readonly<Record<string, CodeFileType>> = {
|
||||
// Maps, not objects: a basename or extension equal to an `Object.prototype` key
|
||||
// (`constructor`, `__proto__`) must miss instead of resolving the inherited member.
|
||||
const FILE_NAME_TYPES: ReadonlyMap<string, CodeFileType> = new Map(Object.entries({
|
||||
'.bash_profile': 'shell',
|
||||
'.bashrc': 'shell',
|
||||
'.env': 'env',
|
||||
@@ -90,7 +92,7 @@ const FILE_NAME_TYPES: Readonly<Record<string, CodeFileType>> = {
|
||||
'package.json': 'node',
|
||||
'podfile': 'ruby',
|
||||
'rakefile': 'ruby',
|
||||
}
|
||||
}))
|
||||
|
||||
const FILE_NAME_PREFIX_TYPES: readonly (readonly [string, CodeFileType])[] = [
|
||||
['dockerfile.', 'docker'],
|
||||
@@ -109,7 +111,7 @@ const FILE_NAME_SUFFIX_TYPES: readonly (readonly [string, CodeFileType])[] = [
|
||||
['.dockerfile', 'docker'],
|
||||
]
|
||||
|
||||
const EXTENSION_TYPES: Readonly<Record<string, CodeFileType>> = {
|
||||
const EXTENSION_TYPES: ReadonlyMap<string, CodeFileType> = new Map(Object.entries({
|
||||
'bash': 'shell',
|
||||
'c': 'c',
|
||||
'c++': 'cpp',
|
||||
@@ -217,7 +219,7 @@ const EXTENSION_TYPES: Readonly<Record<string, CodeFileType>> = {
|
||||
'yml': 'yaml',
|
||||
'zig': 'zig',
|
||||
'zsh': 'shell',
|
||||
}
|
||||
}))
|
||||
|
||||
const LINK_CODE_EXTENSIONS = new Set([
|
||||
'ts',
|
||||
@@ -322,7 +324,7 @@ export function classifyCodeFileType(
|
||||
extension: string,
|
||||
context?: FileTypeProjectContext,
|
||||
): CodeFileType | null {
|
||||
const exact = FILE_NAME_TYPES[name]
|
||||
const exact = FILE_NAME_TYPES.get(name)
|
||||
if (exact !== undefined) return exact
|
||||
for (const [prefix, type] of FILE_NAME_PREFIX_TYPES) {
|
||||
if (name.startsWith(prefix)) return type
|
||||
@@ -334,5 +336,5 @@ export function classifyCodeFileType(
|
||||
const pubspec = Object.entries(context.files).find(([path]) => basename(path).toLowerCase() === 'pubspec.yaml')?.[1]
|
||||
if (pubspec?.includes('flutter:') === true) return 'flutter'
|
||||
}
|
||||
return EXTENSION_TYPES[extension] ?? null
|
||||
return EXTENSION_TYPES.get(extension) ?? null
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { afterEach, describe, expect, it } from 'vitest'
|
||||
import {
|
||||
classifyFileType, fileExtension, FileTypeIcon, type FileTypeKind,
|
||||
} from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import { classifyCodeFileType } from '../src/code-file-types.ts'
|
||||
import css from '../src/FileTypeIcon.module.css'
|
||||
|
||||
afterEach(cleanup)
|
||||
@@ -58,6 +59,30 @@ describe('classifyFileType', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('Object.prototype key names', () => {
|
||||
const keys = ['constructor', '__proto__', 'toString', 'hasOwnProperty']
|
||||
|
||||
it.each(keys.flatMap(key => [
|
||||
[key, 'other'],
|
||||
[`src/${key.toUpperCase()}`, 'other'],
|
||||
[`archive.${key}`, 'other'],
|
||||
[`${key}.md`, 'markdown'],
|
||||
[`${key}.ts`, 'typescript'],
|
||||
] as [string, Exclude<FileTypeKind, 'folder'>][]))('%s → %s', (path, type) => {
|
||||
expect(classifyFileType(path)).toBe(type)
|
||||
})
|
||||
|
||||
it.each(keys)('%s misses the code-file name and extension tables', (key) => {
|
||||
expect(classifyCodeFileType(key, key)).toBeNull()
|
||||
expect(classifyCodeFileType(key, 'ts')).toBe('typescript')
|
||||
})
|
||||
|
||||
it.each(keys.flatMap(key => [key, `notes.${key}`]))('%s renders the generic file glyph', (path) => {
|
||||
const { container } = render(<FileTypeIcon path={path} />)
|
||||
expect(container.querySelector('svg')!.classList.contains(css.other!)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('FileTypeIcon', () => {
|
||||
const types: FileTypeKind[] = [
|
||||
'code', 'excel', 'folder', 'html', 'image', 'markdown', 'other', 'pdf', 'ppt', 'video', 'word',
|
||||
|
||||
Reference in new issue
Block a user