66 * and no binary fixture lives in the repo.
77 */
88
9- import { inflateRawSync } from 'node:zlib ' ;
9+ import { createZip , readZipEntries , readZipEntry } from 'ranuts/utils ' ;
1010
11- const CRC_TABLE = ( ( ) => {
12- const table = new Uint32Array ( 256 ) ;
13- for ( let n = 0 ; n < 256 ; n ++ ) {
14- let c = n ;
15- for ( let k = 0 ; k < 8 ; k ++ ) c = c & 1 ? 0xedb88320 ^ ( c >>> 1 ) : c >>> 1 ;
16- table [ n ] = c >>> 0 ;
17- }
18- return table ;
19- } ) ( ) ;
20-
21- function crc32 ( bytes : Uint8Array ) : number {
22- let c = 0xffffffff ;
23- for ( let i = 0 ; i < bytes . length ; i ++ ) c = CRC_TABLE [ ( c ^ bytes [ i ] ) & 0xff ] ^ ( c >>> 8 ) ;
24- return ( c ^ 0xffffffff ) >>> 0 ;
25- }
26-
27- export function makeStoredZip ( entries : Array < { name : string ; data : Uint8Array | string } > ) : Uint8Array {
28- const enc = new TextEncoder ( ) ;
29- const chunks : number [ ] = [ ] ;
30- const central : number [ ] = [ ] ;
31- const u16 = ( arr : number [ ] , v : number ) => arr . push ( v & 0xff , ( v >> 8 ) & 0xff ) ;
32- const u32 = ( arr : number [ ] , v : number ) => arr . push ( v & 0xff , ( v >> 8 ) & 0xff , ( v >> 16 ) & 0xff , ( v >> 24 ) & 0xff ) ;
33- for ( const { name, data } of entries ) {
34- const nameBytes = enc . encode ( name ) ;
35- const bytes = typeof data === 'string' ? enc . encode ( data ) : data ;
36- const crc = crc32 ( bytes ) ;
37- const offset = chunks . length ;
38- u32 ( chunks , 0x04034b50 ) ;
39- u16 ( chunks , 20 ) ;
40- u16 ( chunks , 0 ) ;
41- u16 ( chunks , 0 ) ;
42- u32 ( chunks , 0 ) ;
43- u32 ( chunks , crc ) ;
44- u32 ( chunks , bytes . length ) ;
45- u32 ( chunks , bytes . length ) ;
46- u16 ( chunks , nameBytes . length ) ;
47- u16 ( chunks , 0 ) ;
48- chunks . push ( ...nameBytes , ...bytes ) ;
49-
50- u32 ( central , 0x02014b50 ) ;
51- u16 ( central , 20 ) ;
52- u16 ( central , 20 ) ;
53- u16 ( central , 0 ) ;
54- u16 ( central , 0 ) ;
55- u32 ( central , 0 ) ;
56- u32 ( central , crc ) ;
57- u32 ( central , bytes . length ) ;
58- u32 ( central , bytes . length ) ;
59- u16 ( central , nameBytes . length ) ;
60- u16 ( central , 0 ) ;
61- u16 ( central , 0 ) ;
62- u16 ( central , 0 ) ;
63- u16 ( central , 0 ) ;
64- u32 ( central , 0 ) ;
65- u32 ( central , offset ) ;
66- central . push ( ...nameBytes ) ;
67- }
68- const centralOffset = chunks . length ;
69- chunks . push ( ...central ) ;
70- u32 ( chunks , 0x06054b50 ) ;
71- u16 ( chunks , 0 ) ;
72- u16 ( chunks , 0 ) ;
73- u16 ( chunks , entries . length ) ;
74- u16 ( chunks , entries . length ) ;
75- u32 ( chunks , central . length ) ;
76- u32 ( chunks , centralOffset ) ;
77- u16 ( chunks , 0 ) ;
78- return new Uint8Array ( chunks ) ;
79- }
11+ /**
12+ * Archive plumbing comes from ranuts (`createZip` / `readZipEntries` /
13+ * `readZipEntry`): ecosystem first, and its reader takes sizes from the
14+ * central directory, which is what makes it survive streaming-written
15+ * archives. Only the OOXML-shaped helpers live here.
16+ */
17+ export const makeStoredZip = ( entries : ReadonlyArray < { name : string ; data : Uint8Array | string } > ) : Uint8Array =>
18+ createZip ( entries ) ;
8019
8120const XML_HEAD = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' ;
8221
@@ -269,59 +208,15 @@ export function buildPptx(title: string): Uint8Array {
269208 ] ) ;
270209}
271210
272- type ZipEntry = { name : string ; method : number ; compressedSize : number ; localOffset : number } ;
273-
274- function zipEntries ( bytes : Uint8Array ) : ZipEntry [ ] {
275- const view = new DataView ( bytes . buffer , bytes . byteOffset , bytes . byteLength ) ;
276- let eocd = - 1 ;
277- for ( let i = bytes . length - 22 ; i >= 0 && i >= bytes . length - 65557 ; i -- ) {
278- if ( view . getUint32 ( i , true ) === 0x06054b50 ) {
279- eocd = i ;
280- break ;
281- }
282- }
283- if ( eocd < 0 ) return [ ] ;
284- const count = view . getUint16 ( eocd + 10 , true ) ;
285- let off = view . getUint32 ( eocd + 16 , true ) ;
286- const entries : ZipEntry [ ] = [ ] ;
287- const dec = new TextDecoder ( ) ;
288- for ( let n = 0 ; n < count ; n ++ ) {
289- if ( view . getUint32 ( off , true ) !== 0x02014b50 ) break ;
290- const method = view . getUint16 ( off + 10 , true ) ;
291- const compressedSize = view . getUint32 ( off + 20 , true ) ;
292- const nameLen = view . getUint16 ( off + 28 , true ) ;
293- const extraLen = view . getUint16 ( off + 30 , true ) ;
294- const commentLen = view . getUint16 ( off + 32 , true ) ;
295- const localOffset = view . getUint32 ( off + 42 , true ) ;
296- entries . push ( {
297- name : dec . decode ( bytes . subarray ( off + 46 , off + 46 + nameLen ) ) ,
298- method,
299- compressedSize,
300- localOffset,
301- } ) ;
302- off += 46 + nameLen + extraLen + commentLen ;
303- }
304- return entries ;
305- }
306-
307211/** Entry names of a zip (central directory walk); enough for L1 checks. */
308212export function zipEntryNames ( bytes : Uint8Array ) : string [ ] {
309- return zipEntries ( bytes ) . map ( ( e ) => e . name ) ;
213+ return readZipEntries ( bytes ) . map ( ( e ) => e . name ) ;
310214}
311215
312216/** Decoded text of one zip entry (stored or deflated) -- for L2 content checks on OOXML parts. */
313- export function zipEntryText ( bytes : Uint8Array , name : string ) : string | null {
314- const entry = zipEntries ( bytes ) . find ( ( e ) => e . name === name ) ;
315- if ( ! entry ) return null ;
316- const view = new DataView ( bytes . buffer , bytes . byteOffset , bytes . byteLength ) ;
317- const lo = entry . localOffset ;
318- if ( view . getUint32 ( lo , true ) !== 0x04034b50 ) return null ;
319- const nameLen = view . getUint16 ( lo + 26 , true ) ;
320- const extraLen = view . getUint16 ( lo + 28 , true ) ;
321- const start = lo + 30 + nameLen + extraLen ;
322- const raw = bytes . subarray ( start , start + entry . compressedSize ) ;
323- const data = entry . method === 8 ? new Uint8Array ( inflateRawSync ( raw ) ) : raw ;
324- return new TextDecoder ( ) . decode ( data ) ;
217+ export async function zipEntryText ( bytes : Uint8Array , name : string ) : Promise < string | null > {
218+ const data = await readZipEntry ( bytes , name ) ;
219+ return data ? new TextDecoder ( ) . decode ( data ) : null ;
325220}
326221
327222const XML_ENTITIES : Record < string , string > = { amp : '&' , lt : '<' , gt : '>' , quot : '"' , apos : "'" } ;
@@ -438,7 +333,7 @@ export function buildXlsx(opts: {
438333 * fingerprints and other invisible off-slide text boxes that OnlyOffice
439334 * drops on save (vendor behavior, not user-visible content).
440335 */
441- export function ooxmlDocumentText ( bytes : Uint8Array ) : string {
336+ export async function ooxmlDocumentText ( bytes : Uint8Array ) : Promise < string > {
442337 const names = zipEntryNames ( bytes ) ;
443338 const parts = names . includes ( 'word/document.xml' )
444339 ? [ 'word/document.xml' ]
@@ -454,7 +349,10 @@ export function ooxmlDocumentText(bytes: Uint8Array): string {
454349 // Ruby guide text (<w:rt>) is a known loss on save (the base word is
455350 // preserved by preprocessDocxRuby); compare base text only.
456351 . replace ( / < w : r t \b [ \s \S ] * ?< \/ w : r t > / g, '' ) ;
457- return parts . map ( ( n ) => ooxmlText ( withoutFields ( withoutHidden ( zipEntryText ( bytes , n ) || '' ) ) ) ) . join ( '\n' ) ;
352+ const texts = await Promise . all (
353+ parts . map ( async ( n ) => ooxmlText ( withoutFields ( withoutHidden ( ( await zipEntryText ( bytes , n ) ) || '' ) ) ) ) ,
354+ ) ;
355+ return texts . join ( '\n' ) ;
458356}
459357
460358/**
0 commit comments