forked from lightningpixel/modly
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathassetLibraryProjection.ts
More file actions
99 lines (89 loc) · 4.24 KB
/
Copy pathassetLibraryProjection.ts
File metadata and controls
99 lines (89 loc) · 4.24 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
import type { AssetCapability, AssetLibraryEntry, AssetLibrarySourceScope } from '../../shared/types/assetLibrary'
export interface ProjectedAssetLibraryEntry extends AssetLibraryEntry {
warnings: string[]
}
export type AssetLibraryOpenTarget =
| { kind: 'self', url: string, workspacePath: string }
| { kind: 'linked-source', url: string, workspacePath: string, sourceWorkspacePath: string }
| { kind: 'unavailable', reason: string }
export interface AssetLibraryCapabilityGroup {
capability: AssetCapability | 'uncategorized'
entries: ProjectedAssetLibraryEntry[]
}
export interface AssetLibraryScopeGroup {
scope: AssetLibrarySourceScope
capabilityGroups: AssetLibraryCapabilityGroup[]
}
function isSafeWorkspacePath(workspacePath: string): boolean {
const normalized = workspacePath.replace(/\\/g, '/').trim()
return /^(Workflows|Exports)\//.test(normalized)
&& !normalized.split('/').includes('..')
&& !/%2e|%2f|%5c/i.test(normalized)
&& !normalized.startsWith('/')
&& !/^[a-zA-Z]:[\\/]/.test(workspacePath)
&& !workspacePath.startsWith('\\\\')
}
function isGlbOrGltf(workspacePath: string): boolean {
return /\.(glb|gltf)$/i.test(workspacePath)
}
export function projectAssetLibraryEntry(entry: AssetLibraryEntry): ProjectedAssetLibraryEntry {
const warnings = [...new Set(entry.warnings)]
if (!isSafeWorkspacePath(entry.workspacePath)) {
return { ...entry, state: 'unsafe', openable: false, nonOpenableReason: entry.nonOpenableReason ?? 'Unsafe workspace path.', warnings }
}
const safeSource = entry.source && isSafeWorkspacePath(entry.source.workspacePath) ? entry.source : undefined
const safeManifest = entry.manifest && isSafeWorkspacePath(entry.manifest.workspacePath) ? entry.manifest : undefined
if (entry.source && !safeSource) warnings.push('Ignored unsafe source workspace path.')
if (entry.manifest && !safeManifest) warnings.push('Ignored unsafe manifest workspace path.')
return { ...entry, source: safeSource, manifest: safeManifest, warnings: [...new Set(warnings)] }
}
export function resolveAssetLibraryOpenTarget(entry: ProjectedAssetLibraryEntry): AssetLibraryOpenTarget {
if (entry.state !== 'ready') {
return { kind: 'unavailable', reason: entry.nonOpenableReason ?? 'Workspace asset is not openable.' }
}
if (entry.source?.workspacePath) {
if (!isSafeWorkspacePath(entry.source.workspacePath) || entry.source.workspacePath === entry.workspacePath || !isGlbOrGltf(entry.source.workspacePath)) {
return { kind: 'unavailable', reason: 'Linked source mesh is unavailable.' }
}
return {
kind: 'linked-source',
url: `/workspace/${entry.source.workspacePath}`,
workspacePath: entry.workspacePath,
sourceWorkspacePath: entry.source.workspacePath,
}
}
if (!entry.openable) {
return { kind: 'unavailable', reason: entry.nonOpenableReason ?? 'Workspace asset is not openable.' }
}
if (!isGlbOrGltf(entry.workspacePath)) {
return { kind: 'unavailable', reason: 'Only safe .glb/.gltf workspace assets are openable in this release.' }
}
return { kind: 'self', url: `/workspace/${entry.workspacePath}`, workspacePath: entry.workspacePath }
}
export function filterAssetLibraryEntries(entries: ProjectedAssetLibraryEntry[], query: string): ProjectedAssetLibraryEntry[] {
const needle = query.trim().toLowerCase()
if (!needle) return entries
return entries.filter((entry) => [
entry.displayName,
entry.workspacePath,
entry.capability ?? '',
entry.sourceScope,
entry.state,
entry.source?.workspacePath ?? '',
entry.manifest?.workspacePath ?? '',
].some((value) => value.toLowerCase().includes(needle)))
}
export function groupAssetLibraryEntries(entries: ProjectedAssetLibraryEntry[]): AssetLibraryScopeGroup[] {
const scopes = [...new Set(entries.map((entry) => entry.sourceScope))].sort()
return scopes.map((scope) => {
const scopeEntries = entries.filter((entry) => entry.sourceScope === scope)
const capabilities = [...new Set(scopeEntries.map((entry) => entry.capability ?? 'uncategorized'))].sort()
return {
scope,
capabilityGroups: capabilities.map((capability) => ({
capability,
entries: scopeEntries.filter((entry) => (entry.capability ?? 'uncategorized') === capability),
})),
}
})
}