| 18 | } |
| 19 | |
| 20 | export class XlsxParser implements FileParser { |
| 21 | /** |
| 22 | * Read the file into a buffer and delegate to {@link parseBuffer} so the |
| 23 | * decompression-bomb guard runs before SheetJS inflates the workbook. |
| 24 | */ |
| 25 | async parseFile(filePath: string): Promise<FileParseResult> { |
| 26 | try { |
| 27 | if (!filePath) { |
| 28 | throw new Error('No file path provided') |
| 29 | } |
| 30 | |
| 31 | if (!existsSync(filePath)) { |
| 32 | throw new Error(`File not found: ${filePath}`) |
| 33 | } |
| 34 | |
| 35 | logger.info(`Parsing XLSX file: ${filePath}`) |
| 36 | |
| 37 | const buffer = await readFile(filePath) |
| 38 | return this.parseBuffer(buffer) |
| 39 | } catch (error) { |
| 40 | logger.error('XLSX file parsing error:', error) |
| 41 | throw new Error(`Failed to parse XLSX file: ${(error as Error).message}`) |
| 42 | } |
| 43 | } |
| 44 | |
| 45 | async parseBuffer(buffer: Buffer): Promise<FileParseResult> { |
| 46 | try { |
| 47 | const bufferSize = buffer.length |
| 48 | logger.info( |
| 49 | `Parsing XLSX buffer, size: ${bufferSize} bytes (${(bufferSize / 1024 / 1024).toFixed(2)} MB)` |
| 50 | ) |
| 51 | |
| 52 | if (!buffer || buffer.length === 0) { |
| 53 | throw new Error('Empty buffer provided') |
| 54 | } |
| 55 | |
| 56 | assertOoxmlArchiveWithinLimits(buffer) |
| 57 | |
| 58 | const workbook = XLSX.read(buffer, { |
| 59 | type: 'buffer', |
| 60 | dense: true, // Use dense mode for better memory efficiency |
| 61 | sheetStubs: false, // Don't create stub cells |
| 62 | }) |
| 63 | |
| 64 | return this.processWorkbook(workbook) |
| 65 | } catch (error) { |
| 66 | logger.error('XLSX buffer parsing error:', error) |
| 67 | throw new Error(`Failed to parse XLSX buffer: ${(error as Error).message}`) |
| 68 | } |
| 69 | } |
| 70 | |
| 71 | private processWorkbook(workbook: XLSX.WorkBook): FileParseResult { |
| 72 | const sheetNames = workbook.SheetNames |
| 73 | let content = '' |
| 74 | let totalRows = 0 |
| 75 | let truncated = false |
| 76 | let contentSize = 0 |
| 77 | const sampledData: any[] = [] |
nothing calls this directly
no outgoing calls
no test coverage detected