@@ -5,6 +5,15 @@ import { loadZipPreview, readZipPreviewResponse } from "./zipPreview";
55
66const signal = ( ) => new AbortController ( ) . signal ;
77
8+ function png ( width = 800 , height = 600 ) {
9+ const bytes = new Uint8Array ( 24 ) ;
10+ bytes . set ( [ 137 , 80 , 78 , 71 , 13 , 10 , 26 , 10 , 0 , 0 , 0 , 13 , 73 , 72 , 68 , 82 ] ) ;
11+ const view = new DataView ( bytes . buffer ) ;
12+ view . setUint32 ( 16 , width ) ;
13+ view . setUint32 ( 20 , height ) ;
14+ return bytes ;
15+ }
16+
817async function archive ( zip : JSZip ) {
918 return new Blob ( [ await zip . generateAsync ( { type : "arraybuffer" , compression : "DEFLATE" } ) ] ) ;
1019}
@@ -183,19 +192,129 @@ describe("loadZipPreview", () => {
183192 ] ) ;
184193 } ) ;
185194
186- it ( "reads the selected raster image into a typed blob " , async ( ) => {
187- const zip = new JSZip ( ) . file ( "photo.jpg" , new Uint8Array ( [ 0xff , 0xd8 , 0xff ] ) ) ;
195+ it ( "checks the image bytes even when the extension declares a different image type " , async ( ) => {
196+ const zip = new JSZip ( ) . file ( "photo.jpg" , png ( ) ) ;
188197 const preview = await loadZipPreview ( await archive ( zip ) , signal ( ) ) ;
189198 const image = await preview . readFile ( preview . entries [ 0 ] ! , signal ( ) ) ;
190199
191200 expect ( image . type ) . toBe ( "image/jpeg" ) ;
192- expect ( new Uint8Array ( await image . arrayBuffer ( ) ) ) . toEqual ( new Uint8Array ( [ 0xff , 0xd8 , 0xff ] ) ) ;
201+ expect ( new Uint8Array ( await image . arrayBuffer ( ) ) ) . toEqual ( png ( ) ) ;
202+ } ) ;
203+
204+ it . each ( [
205+ [ "photo.png" , png ( 10000 , 10000 ) ] ,
206+ [ "photo.jpg" , png ( 4001 , 4000 ) ] ,
207+ [ "photo.webp" , png ( 8193 , 1 ) ] ,
208+ [ "photo.jpg" , new Uint8Array ( [ 255 , 216 , 255 , 192 , 0 , 17 , 8 , 39 , 16 , 39 , 16 ] ) ] ,
209+ [
210+ "photo.webp" ,
211+ new Uint8Array ( [
212+ 82 , 73 , 70 , 70 , 22 , 0 , 0 , 0 , 87 , 69 , 66 , 80 , 86 , 80 , 56 , 88 , 10 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 15 ,
213+ 39 , 0 , 15 , 39 , 0 ,
214+ ] ) ,
215+ ] ,
216+ [
217+ "photo.gif" ,
218+ new Uint8Array ( [
219+ 71 , 73 , 70 , 56 , 57 , 97 , 1 , 0 , 1 , 0 , 0 , 0 , 0 , 44 , 0 , 0 , 0 , 0 , 16 , 39 , 16 , 39 , 0 ,
220+ ] ) ,
221+ ] ,
222+ ] ) ( "rejects oversized decoded images before creating a Blob: %s" , async ( name , bytes ) => {
223+ const preview = await loadZipPreview ( await archive ( new JSZip ( ) . file ( name , bytes ) ) , signal ( ) ) ;
224+ const blob = vi . spyOn ( globalThis , "Blob" ) ;
225+ try {
226+ await expect ( preview . readFile ( preview . entries [ 0 ] ! , signal ( ) ) ) . rejects . toThrow (
227+ / 1 6 m i l l i o n p i x e l s .* 8 , 1 9 2 .* D o w n l o a d t h e Z I P / i,
228+ ) ;
229+ expect ( blob ) . not . toHaveBeenCalled ( ) ;
230+ } finally {
231+ blob . mockRestore ( ) ;
232+ }
233+ } ) ;
234+
235+ it . each ( [
236+ [ "photo.png" , png ( 4000 , 4000 ) ] ,
237+ [ "photo.png" , png ( 8192 , 1 ) ] ,
238+ [
239+ "photo.gif" ,
240+ new Uint8Array ( [
241+ 71 , 73 , 70 , 56 , 57 , 97 , 32 , 3 , 88 , 2 , 0 , 0 , 0 , 44 , 0 , 0 , 0 , 0 , 32 , 3 , 88 , 2 , 0 ,
242+ ] ) ,
243+ ] ,
244+ [
245+ "photo.gif" ,
246+ Uint8Array . from ( atob ( "R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" ) , ( c ) =>
247+ c . charCodeAt ( 0 ) ,
248+ ) ,
249+ ] ,
250+ [ "photo.jpg" , new Uint8Array ( [ 255 , 216 , 255 , 192 , 0 , 17 , 8 , 2 , 88 , 3 , 32 ] ) ] ,
251+ [
252+ "photo.webp" ,
253+ new Uint8Array ( [
254+ 82 , 73 , 70 , 70 , 22 , 0 , 0 , 0 , 87 , 69 , 66 , 80 , 86 , 80 , 56 , 32 , 10 , 0 , 0 , 0 , 0 , 0 , 0 , 157 , 1 ,
255+ 42 , 32 , 3 , 88 , 2 ,
256+ ] ) ,
257+ ] ,
258+ [
259+ "photo.webp" ,
260+ new Uint8Array ( [
261+ 82 , 73 , 70 , 70 , 17 , 0 , 0 , 0 , 87 , 69 , 66 , 80 , 86 , 80 , 56 , 76 , 5 , 0 , 0 , 0 , 47 , 31 , 195 , 149 ,
262+ 0 ,
263+ ] ) ,
264+ ] ,
265+ [
266+ "photo.webp" ,
267+ new Uint8Array ( [
268+ 82 , 73 , 70 , 70 , 22 , 0 , 0 , 0 , 87 , 69 , 66 , 80 , 86 , 80 , 56 , 88 , 10 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 31 , 3 ,
269+ 0 , 87 , 2 , 0 ,
270+ ] ) ,
271+ ] ,
272+ ] ) ( "keeps bounded %s images available to the shared preview" , async ( name , bytes ) => {
273+ const preview = await loadZipPreview ( await archive ( new JSZip ( ) . file ( name , bytes ) ) , signal ( ) ) ;
274+ const image = await preview . readFile ( preview . entries [ 0 ] ! , signal ( ) ) ;
275+ expect ( new Uint8Array ( await image . arrayBuffer ( ) ) ) . toEqual ( bytes ) ;
276+ expect ( image . type ) . toMatch ( / ^ i m a g e \/ / ) ;
277+ } ) ;
278+
279+ it . each ( [
280+ [ "photo.svg" , '<svg xmlns="http://www.w3.org/2000/svg" width="10000" height="10000"/>' ] ,
281+ [ "photo.avif" , "unsupported header" ] ,
282+ [ "photo.bmp" , "BM" ] ,
283+ [ "photo.ico" , "unsupported header" ] ,
284+ [ "photo.jpg" , new Uint8Array ( [ 255 , 216 , 255 ] ) ] ,
285+ [ "photo.png" , png ( 0 , 600 ) ] ,
286+ [ "photo.gif" , png ( ) . subarray ( 0 , 20 ) ] ,
287+ [ "photo.gif" , new Uint8Array ( [ 71 , 73 , 70 , 56 , 57 , 97 , 1 , 0 , 1 , 0 , 0 , 0 , 0 , 33 , 249 , 255 ] ) ] ,
288+ [ "photo.png" , '<svg xmlns="http://www.w3.org/2000/svg"/>' ] ,
289+ ] ) ( "blocks unreadable dimensions without decoding: %s" , async ( name , bytes ) => {
290+ const preview = await loadZipPreview ( await archive ( new JSZip ( ) . file ( name , bytes ) ) , signal ( ) ) ;
291+ await expect ( preview . readFile ( preview . entries [ 0 ] ! , signal ( ) ) ) . rejects . toThrow (
292+ / C a n n o t s a f e l y p r e v i e w t h i s i m a g e .* D o w n l o a d t h e Z I P / i,
293+ ) ;
294+ } ) ;
295+
296+ it ( "reads a JPEG frame across extracted chunks but rejects headers beyond the scan limit" , async ( ) => {
297+ for ( const segments of [ 1 , 5 ] ) {
298+ const bytes = new Uint8Array ( 2 + segments * 65537 + 9 ) ;
299+ bytes . set ( [ 255 , 216 ] ) ;
300+ for ( let index = 0 ; index < segments ; index ++ ) {
301+ bytes . set ( [ 255 , 224 , 255 , 255 ] , 2 + index * 65537 ) ;
302+ }
303+ bytes . set ( [ 255 , 192 , 0 , 17 , 8 , 2 , 88 , 3 , 32 ] , bytes . length - 9 ) ;
304+ const preview = await loadZipPreview (
305+ await archive ( new JSZip ( ) . file ( "photo.jpg" , bytes ) ) ,
306+ signal ( ) ,
307+ ) ;
308+ const reading = preview . readFile ( preview . entries [ 0 ] ! , signal ( ) ) ;
309+ if ( segments === 1 ) expect ( ( await reading ) . size ) . toBe ( bytes . length ) ;
310+ else await expect ( reading ) . rejects . toThrow ( / C a n n o t s a f e l y p r e v i e w / i) ;
311+ }
193312 } ) ;
194313
195314 it . each ( [ "photos\\image.png" , "photos\\\\image.png" , "photos//image.png" ] ) (
196315 "keeps folder and file paths consistent for %s" ,
197316 async ( path ) => {
198- const zip = new JSZip ( ) . file ( path , new Uint8Array ( [ 1 , 2 , 3 ] ) , { createFolders : false } ) ;
317+ const zip = new JSZip ( ) . file ( path , png ( ) , { createFolders : false } ) ;
199318 zip . file ( path . slice ( 0 , - "image.png" . length ) , null , { dir : true , createFolders : false } ) ;
200319 const preview = await loadZipPreview ( await archive ( zip ) , signal ( ) ) ;
201320
@@ -205,12 +324,12 @@ describe("loadZipPreview", () => {
205324 path : "photos/image.png" ,
206325 name : "image.png" ,
207326 isDirectory : false ,
208- sizeBytes : 3 ,
327+ sizeBytes : 24 ,
209328 mimeType : "image/png" ,
210329 } ,
211330 ] ) ;
212331 const image = await preview . readFile ( preview . entries [ 1 ] ! , signal ( ) ) ;
213- expect ( new Uint8Array ( await image . arrayBuffer ( ) ) ) . toEqual ( new Uint8Array ( [ 1 , 2 , 3 ] ) ) ;
332+ expect ( new Uint8Array ( await image . arrayBuffer ( ) ) ) . toEqual ( png ( ) ) ;
214333 } ,
215334 ) ;
216335
@@ -237,7 +356,6 @@ describe("loadZipPreview", () => {
237356 [ "document.pdf" , "%PDF-1.4" , "application/pdf" ] ,
238357 [ "clip.mp4" , "media" , "video/mp4" ] ,
239358 [ "sound.mp3" , "media" , "audio/mpeg" ] ,
240- [ "image.svg" , "<svg/>" , "image/svg+xml" ] ,
241359 ] ) (
242360 "extracts only the selected file %s for the shared viewer" ,
243361 async ( name , contents , mimeType ) => {
@@ -394,7 +512,9 @@ describe("loadZipPreview", () => {
394512 } ) ;
395513
396514 it ( "cancels image reads after the stream starts" , async ( ) => {
397- const zip = new JSZip ( ) . file ( "photo.png" , new Uint8Array ( 64 * 1024 ) ) ;
515+ const contents = new Uint8Array ( 64 * 1024 ) ;
516+ contents . set ( png ( ) ) ;
517+ const zip = new JSZip ( ) . file ( "photo.png" , contents ) ;
398518 const preview = await loadZipPreview ( await archive ( zip ) , signal ( ) ) ;
399519 const controller = new AbortController ( ) ;
400520 const prototype = Object . getPrototypeOf ( zip . file ( "photo.png" ) ) as {
0 commit comments