chore: import upstream snapshot with attribution
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,269 @@
|
||||
/**
|
||||
* PPTX zip archive parser.
|
||||
* Extracts and categorizes all files from a .pptx (which is a zip archive).
|
||||
*/
|
||||
|
||||
import type { JSZipObject } from 'jszip'
|
||||
import JSZip from 'jszip'
|
||||
|
||||
export interface PptxFiles {
|
||||
contentTypes: string
|
||||
presentation: string
|
||||
presentationRels: string
|
||||
slides: Map<string, string>
|
||||
slideRels: Map<string, string>
|
||||
slideLayouts: Map<string, string>
|
||||
slideLayoutRels: Map<string, string>
|
||||
slideMasters: Map<string, string>
|
||||
slideMasterRels: Map<string, string>
|
||||
themes: Map<string, string>
|
||||
media: Map<string, Uint8Array>
|
||||
tableStyles?: string
|
||||
charts: Map<string, string> // ppt/charts/chart*.xml
|
||||
chartStyles: Map<string, string> // ppt/charts/style*.xml
|
||||
chartColors: Map<string, string> // ppt/charts/colors*.xml
|
||||
diagramDrawings: Map<string, string> // ppt/diagrams/drawing*.xml (SmartArt fallback)
|
||||
}
|
||||
|
||||
export interface ZipParseLimits {
|
||||
/** Maximum number of non-directory entries in the zip archive. */
|
||||
maxEntries?: number
|
||||
/** Maximum uncompressed size for any single entry (bytes). */
|
||||
maxEntryUncompressedBytes?: number
|
||||
/** Maximum total uncompressed size across all entries (bytes). */
|
||||
maxTotalUncompressedBytes?: number
|
||||
/** Maximum uncompressed size across media entries under `ppt/media/` (bytes). */
|
||||
maxMediaBytes?: number
|
||||
/** Maximum concurrent zip entry reads during parsing. */
|
||||
maxConcurrency?: number
|
||||
}
|
||||
|
||||
function throwZipLimitExceeded(reason: string): never {
|
||||
throw new Error(`PPTX zip limit exceeded: ${reason}`)
|
||||
}
|
||||
|
||||
function readUncompressedSize(file: JSZipObject): number | undefined {
|
||||
const data = (file as JSZipObject & { _data?: { uncompressedSize?: number } })._data
|
||||
const size = data?.uncompressedSize
|
||||
return typeof size === 'number' && Number.isFinite(size) ? size : undefined
|
||||
}
|
||||
|
||||
async function mapWithConcurrency<T>(
|
||||
items: T[],
|
||||
concurrency: number,
|
||||
mapper: (item: T) => Promise<void>
|
||||
): Promise<void> {
|
||||
if (items.length === 0) return
|
||||
const workerCount = Math.min(concurrency, items.length)
|
||||
let cursor = 0
|
||||
|
||||
const workers = Array.from({ length: workerCount }, async () => {
|
||||
while (true) {
|
||||
const index = cursor++
|
||||
if (index >= items.length) return
|
||||
await mapper(items[index])
|
||||
}
|
||||
})
|
||||
|
||||
await Promise.all(workers)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a .pptx file buffer and extract all relevant files, categorized by type.
|
||||
*/
|
||||
export async function parseZip(
|
||||
buffer: ArrayBuffer,
|
||||
limits: ZipParseLimits = {}
|
||||
): Promise<PptxFiles> {
|
||||
const maxConcurrency = limits.maxConcurrency ?? 8
|
||||
if (!Number.isInteger(maxConcurrency) || maxConcurrency < 1) {
|
||||
throwZipLimitExceeded(`maxConcurrency ${limits.maxConcurrency} must be an integer >= 1`)
|
||||
}
|
||||
|
||||
const zip = await JSZip.loadAsync(buffer)
|
||||
const entries = Object.entries(zip.files).filter(([, file]) => !file.dir)
|
||||
|
||||
if (limits.maxEntries !== undefined && entries.length > limits.maxEntries) {
|
||||
throwZipLimitExceeded(`entries ${entries.length} > maxEntries ${limits.maxEntries}`)
|
||||
}
|
||||
|
||||
const knownSizeByPath = new Map<string, number>()
|
||||
let knownTotalBytes = 0
|
||||
let knownMediaBytes = 0
|
||||
|
||||
for (const [rawPath, file] of entries) {
|
||||
const normalizedPath = rawPath.replace(/\\/g, '/')
|
||||
const size = readUncompressedSize(file)
|
||||
if (size === undefined) continue
|
||||
|
||||
knownSizeByPath.set(normalizedPath, size)
|
||||
|
||||
if (limits.maxEntryUncompressedBytes !== undefined && size > limits.maxEntryUncompressedBytes) {
|
||||
throwZipLimitExceeded(
|
||||
`${normalizedPath} is ${size} bytes > maxEntryUncompressedBytes ${limits.maxEntryUncompressedBytes}`
|
||||
)
|
||||
}
|
||||
|
||||
knownTotalBytes += size
|
||||
if (
|
||||
limits.maxTotalUncompressedBytes !== undefined &&
|
||||
knownTotalBytes > limits.maxTotalUncompressedBytes
|
||||
) {
|
||||
throwZipLimitExceeded(
|
||||
`total uncompressed bytes ${knownTotalBytes} > maxTotalUncompressedBytes ${limits.maxTotalUncompressedBytes}`
|
||||
)
|
||||
}
|
||||
|
||||
if (normalizedPath.startsWith('ppt/media/')) {
|
||||
knownMediaBytes += size
|
||||
if (limits.maxMediaBytes !== undefined && knownMediaBytes > limits.maxMediaBytes) {
|
||||
throwZipLimitExceeded(
|
||||
`media bytes ${knownMediaBytes} > maxMediaBytes ${limits.maxMediaBytes}`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const result: PptxFiles = {
|
||||
contentTypes: '',
|
||||
presentation: '',
|
||||
presentationRels: '',
|
||||
slides: new Map(),
|
||||
slideRels: new Map(),
|
||||
slideLayouts: new Map(),
|
||||
slideLayoutRels: new Map(),
|
||||
slideMasters: new Map(),
|
||||
slideMasterRels: new Map(),
|
||||
themes: new Map(),
|
||||
media: new Map(),
|
||||
charts: new Map(),
|
||||
chartStyles: new Map(),
|
||||
chartColors: new Map(),
|
||||
diagramDrawings: new Map(),
|
||||
}
|
||||
|
||||
let unknownMediaBytes = 0
|
||||
|
||||
await mapWithConcurrency(entries, maxConcurrency, async ([path, file]) => {
|
||||
const normalizedPath = path.replace(/\\/g, '/')
|
||||
|
||||
// --- Content Types ---
|
||||
if (normalizedPath === '[Content_Types].xml') {
|
||||
result.contentTypes = await file.async('string')
|
||||
return
|
||||
}
|
||||
|
||||
// --- Presentation ---
|
||||
if (normalizedPath === 'ppt/presentation.xml') {
|
||||
result.presentation = await file.async('string')
|
||||
return
|
||||
}
|
||||
|
||||
// --- Presentation Rels ---
|
||||
if (normalizedPath === 'ppt/_rels/presentation.xml.rels') {
|
||||
result.presentationRels = await file.async('string')
|
||||
return
|
||||
}
|
||||
|
||||
// --- Table Styles ---
|
||||
if (normalizedPath === 'ppt/tableStyles.xml') {
|
||||
result.tableStyles = await file.async('string')
|
||||
return
|
||||
}
|
||||
|
||||
// --- Media (binary) ---
|
||||
if (normalizedPath.startsWith('ppt/media/')) {
|
||||
const bytes = await file.async('uint8array')
|
||||
if (!knownSizeByPath.has(normalizedPath)) {
|
||||
const size = bytes.byteLength
|
||||
if (
|
||||
limits.maxEntryUncompressedBytes !== undefined &&
|
||||
size > limits.maxEntryUncompressedBytes
|
||||
) {
|
||||
throwZipLimitExceeded(
|
||||
`${normalizedPath} is ${size} bytes > maxEntryUncompressedBytes ${limits.maxEntryUncompressedBytes}`
|
||||
)
|
||||
}
|
||||
unknownMediaBytes += size
|
||||
if (
|
||||
limits.maxMediaBytes !== undefined &&
|
||||
knownMediaBytes + unknownMediaBytes > limits.maxMediaBytes
|
||||
) {
|
||||
throwZipLimitExceeded(
|
||||
`media bytes ${knownMediaBytes + unknownMediaBytes} > maxMediaBytes ${limits.maxMediaBytes}`
|
||||
)
|
||||
}
|
||||
}
|
||||
result.media.set(normalizedPath, bytes)
|
||||
return
|
||||
}
|
||||
|
||||
// --- Slide Rels (must check before slides to avoid false match) ---
|
||||
if (/^ppt\/slides\/_rels\/slide\d+\.xml\.rels$/.test(normalizedPath)) {
|
||||
result.slideRels.set(normalizedPath, await file.async('string'))
|
||||
return
|
||||
}
|
||||
|
||||
// --- Slides ---
|
||||
if (/^ppt\/slides\/slide\d+\.xml$/.test(normalizedPath)) {
|
||||
result.slides.set(normalizedPath, await file.async('string'))
|
||||
return
|
||||
}
|
||||
|
||||
// --- Slide Layout Rels ---
|
||||
if (/^ppt\/slideLayouts\/_rels\/slideLayout\d+\.xml\.rels$/.test(normalizedPath)) {
|
||||
result.slideLayoutRels.set(normalizedPath, await file.async('string'))
|
||||
return
|
||||
}
|
||||
|
||||
// --- Slide Layouts ---
|
||||
if (/^ppt\/slideLayouts\/slideLayout\d+\.xml$/.test(normalizedPath)) {
|
||||
result.slideLayouts.set(normalizedPath, await file.async('string'))
|
||||
return
|
||||
}
|
||||
|
||||
// --- Slide Master Rels ---
|
||||
if (/^ppt\/slideMasters\/_rels\/slideMaster\d+\.xml\.rels$/.test(normalizedPath)) {
|
||||
result.slideMasterRels.set(normalizedPath, await file.async('string'))
|
||||
return
|
||||
}
|
||||
|
||||
// --- Slide Masters ---
|
||||
if (/^ppt\/slideMasters\/slideMaster\d+\.xml$/.test(normalizedPath)) {
|
||||
result.slideMasters.set(normalizedPath, await file.async('string'))
|
||||
return
|
||||
}
|
||||
|
||||
// --- Themes ---
|
||||
if (/^ppt\/theme\/theme\d+\.xml$/.test(normalizedPath)) {
|
||||
result.themes.set(normalizedPath, await file.async('string'))
|
||||
return
|
||||
}
|
||||
|
||||
// --- Charts ---
|
||||
if (/^ppt\/charts\/chart\d+\.xml$/.test(normalizedPath)) {
|
||||
result.charts.set(normalizedPath, await file.async('string'))
|
||||
return
|
||||
}
|
||||
|
||||
// --- Chart Styles ---
|
||||
if (/^ppt\/charts\/style\d+\.xml$/.test(normalizedPath)) {
|
||||
result.chartStyles.set(normalizedPath, await file.async('string'))
|
||||
return
|
||||
}
|
||||
|
||||
// --- Chart Colors ---
|
||||
if (/^ppt\/charts\/colors\d+\.xml$/.test(normalizedPath)) {
|
||||
result.chartColors.set(normalizedPath, await file.async('string'))
|
||||
return
|
||||
}
|
||||
|
||||
// --- Diagram Drawings (SmartArt fallback) ---
|
||||
if (/^ppt\/diagrams\/drawing\d+\.xml$/.test(normalizedPath)) {
|
||||
result.diagramDrawings.set(normalizedPath, await file.async('string'))
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
Reference in New Issue
Block a user