(v: unknown)
| 36 | } |
| 37 | |
| 38 | export function validateAssetName(v: unknown): string | null { |
| 39 | if (typeof v !== 'string') return null; |
| 40 | const trimmed = v.trim(); |
| 41 | if (trimmed.length < 1 || trimmed.length > 120) return null; |
| 42 | // No path separators, control chars, or characters Windows/macOS can't store. |
| 43 | if (ASSET_FORBIDDEN_RE.test(trimmed)) return null; |
| 44 | // Block leading dots / tildes (hidden files, home expansion) and any `..` segment. |
| 45 | if (trimmed.startsWith('.') || trimmed.startsWith('~')) return null; |
| 46 | if (trimmed === '..' || trimmed.split(/[/\\]/).includes('..')) return null; |
| 47 | // Require an extension so authors get sensible MIME / dev-server behavior. |
| 48 | const dot = trimmed.lastIndexOf('.'); |
| 49 | if (dot <= 0 || dot === trimmed.length - 1) return null; |
| 50 | return trimmed; |
| 51 | } |
| 52 | |
| 53 | export function resolveAssetsDir(slidesRoot: string, slideId: string): string | null { |
| 54 | if (!SLIDE_ID_RE.test(slideId)) return null; |
no outgoing calls
no test coverage detected