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

This commit is contained in:
wehub-resource-sync
2026-07-13 13:20:55 +08:00
commit d25d482dc2
13754 changed files with 4996608 additions and 0 deletions
@@ -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
}