| 1 | import { unzipSync } from 'fflate' |
| 2 | |
| 3 | export const normalizeSlidePackArchivePath = (rawName: string): string | null => { |
| 4 | const normalized = rawName.replace(/\\/g, '/').replace(/^\/+/, '') |
| 5 | if (!normalized || normalized.endsWith('/')) return null |
| 6 | const parts = normalized.split('/').filter(Boolean) |
| 7 | if (parts.length === 0 || parts.some((part) => part === '..' || part === '.')) return null |
| 8 | return parts.join('/') |
| 9 | } |
| 10 | |
| 11 | export const tryReadSlidePackZip = (zipData: Uint8Array): Record<string, Uint8Array> | null => { |
| 12 | try { |
| 13 | return unzipSync(zipData) |
| 14 | } catch { |
| 15 | return null |
| 16 | } |
| 17 | } |
| 18 | |
| 19 | export const archiveHasRootIndexHtml = (zipData: Uint8Array): boolean => { |
| 20 | const files = tryReadSlidePackZip(zipData) |
| 21 | if (!files) return false |
| 22 | return Object.keys(files).some((rawName) => { |
| 23 | const relativePath = normalizeSlidePackArchivePath(rawName) |
| 24 | return relativePath?.toLowerCase() === 'index.html' |
| 25 | }) |
| 26 | } |
| 27 | |
| 28 | export const findSlidePackResourceZipInsideZip = (zipData: Uint8Array): Uint8Array | null => { |
| 29 | const files = tryReadSlidePackZip(zipData) |
| 30 | if (!files) return null |
| 31 | const candidates = Object.entries(files) |
| 32 | .map(([name, data]) => ({ name: normalizeSlidePackArchivePath(name), data })) |
| 33 | .filter((entry): entry is { name: string; data: Uint8Array } => { |
| 34 | if (!entry.name) return false |
| 35 | return entry.name.toLowerCase().endsWith('.app/contents/resources/slides.zip') |
| 36 | }) |
| 37 | .filter((entry) => archiveHasRootIndexHtml(entry.data)) |
| 38 | |
| 39 | if (candidates.length !== 1) return null |
| 40 | return candidates[0].data |
| 41 | } |
| 42 |