usd-gltf 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/limits.js ADDED
@@ -0,0 +1,101 @@
1
+ // Opt-in resource limits for a conversion: decoded data, serialized output and cancellation.
2
+
3
+ /**
4
+ * Thrown when a conversion would go over one of its limits. The message and fields describe sizes
5
+ * only, never input data or file names.
6
+ */
7
+ export class ConversionLimitError extends Error {
8
+
9
+ /**
10
+ * @param {{ stage: string, resource: 'decoded'|'output', limit: number, attempted: number }} info
11
+ */
12
+ constructor( { stage, resource, limit, attempted } ) {
13
+
14
+ super( `usd-gltf: ${ stage } needs ${ attempted } ${ resource } bytes, over the limit of ${ limit }` );
15
+ this.name = 'ConversionLimitError';
16
+ this.code = 'USD_GLTF_LIMIT';
17
+ this.stage = stage;
18
+ this.resource = resource;
19
+ this.limit = limit;
20
+ this.attempted = attempted;
21
+
22
+ }
23
+
24
+ }
25
+
26
+ /**
27
+ * Tracks one conversion against its limits. Readers and converters charge decoded bytes before
28
+ * allocating them; writers report their size before growing.
29
+ */
30
+ export class Budget {
31
+
32
+ /**
33
+ * @param {{ maxDecodedBytes?: number, maxOutputBytes?: number, signal?: AbortSignal }} [options]
34
+ */
35
+ constructor( { maxDecodedBytes = Infinity, maxOutputBytes = Infinity, signal = null } = {} ) {
36
+
37
+ this.maxDecodedBytes = maxDecodedBytes;
38
+ this.maxOutputBytes = maxOutputBytes;
39
+ this.signal = signal;
40
+ this.decodedBytes = 0;
41
+
42
+ }
43
+
44
+ /**
45
+ * Adds to the running total of decoded data: accessors, arrays, decompressed sections and images.
46
+ * @param {number} bytes
47
+ * @param {string} stage
48
+ */
49
+ decode( bytes, stage ) {
50
+
51
+ checkSize( bytes, stage );
52
+ this.check();
53
+ this.decodedBytes += bytes;
54
+ if ( this.decodedBytes > this.maxDecodedBytes ) throw new ConversionLimitError( { stage, resource: 'decoded', limit: this.maxDecodedBytes, attempted: this.decodedBytes } );
55
+
56
+ }
57
+
58
+ /**
59
+ * Checks the size an output buffer is about to reach.
60
+ * @param {number} bytes
61
+ * @param {string} stage
62
+ */
63
+ output( bytes, stage ) {
64
+
65
+ checkSize( bytes, stage );
66
+ if ( bytes > this.maxOutputBytes ) throw new ConversionLimitError( { stage, resource: 'output', limit: this.maxOutputBytes, attempted: bytes } );
67
+
68
+ }
69
+
70
+ // Throws the signal's reason once the conversion is cancelled.
71
+ check() {
72
+
73
+ this.signal?.throwIfAborted();
74
+
75
+ }
76
+
77
+ }
78
+
79
+ /**
80
+ * A size computed from counts in a file must be a whole number of bytes that fits in an array.
81
+ * @param {number} bytes
82
+ * @param {string} stage
83
+ */
84
+ export function checkSize( bytes, stage ) {
85
+
86
+ if ( ! Number.isSafeInteger( bytes ) || bytes < 0 ) throw new Error( `usd-gltf: invalid size in ${ stage }` );
87
+
88
+ }
89
+
90
+ /**
91
+ * For catch blocks that skip one failed asset and carry on: rethrows what must end the whole
92
+ * conversion instead, a limit error or cancellation.
93
+ * @param {unknown} error
94
+ * @param {Budget} [budget]
95
+ */
96
+ export function rethrowFatal( error, budget ) {
97
+
98
+ budget?.check();
99
+ if ( error instanceof ConversionLimitError ) throw error;
100
+
101
+ }
package/src/usd/stage.js CHANGED
@@ -5,6 +5,7 @@
5
5
  import { AssetPath, ListOp } from './model.js';
6
6
  import { readUSDA } from './usda/reader.js';
7
7
  import { readZip } from './usdz.js';
8
+ import { Budget, rethrowFatal } from '../limits.js';
8
9
 
9
10
  const decoder = new TextDecoder();
10
11
 
@@ -80,9 +81,10 @@ export function resolveAssetPath( anchor, assetPath ) {
80
81
  // Reads bytes for identifiers, opening packages (including nested ones) through the user resolver.
81
82
  export class AssetReader {
82
83
 
83
- constructor( resolver ) {
84
+ constructor( resolver, budget = new Budget() ) {
84
85
 
85
86
  this.resolver = resolver;
87
+ this.budget = budget;
86
88
  this.packages = new Map();
87
89
  this.files = new Map();
88
90
 
@@ -123,7 +125,7 @@ export class AssetReader {
123
125
 
124
126
  async openPackage( identifier ) {
125
127
 
126
- if ( ! this.packages.has( identifier ) ) this.packages.set( identifier, readZip( await this.read( identifier ) ) );
128
+ if ( ! this.packages.has( identifier ) ) this.packages.set( identifier, readZip( await this.read( identifier ), { budget: this.budget } ) );
127
129
  return this.packages.get( identifier );
128
130
 
129
131
  }
@@ -140,7 +142,7 @@ export class AssetReader {
140
142
 
141
143
  }
142
144
 
143
- async function parseLayer( bytes, identifier ) {
145
+ async function parseLayer( bytes, identifier, budget ) {
144
146
 
145
147
  const format = detectLayerFormat( bytes );
146
148
  if ( format === 'usda' ) return readUSDA( bytes, identifier );
@@ -148,7 +150,7 @@ async function parseLayer( bytes, identifier ) {
148
150
  if ( format === 'usdc' ) {
149
151
 
150
152
  const { readUSDC } = await import( './usdc/reader.js' );
151
- return readUSDC( bytes, identifier );
153
+ return readUSDC( bytes, identifier, { budget } );
152
154
 
153
155
  }
154
156
 
@@ -185,14 +187,15 @@ async function loadLayers( reader, rootIdentifier ) {
185
187
 
186
188
  if ( detectLayerFormat( bytes ) === 'usdz' ) {
187
189
 
188
- reader.addPackage( identifier, readZip( bytes ) );
190
+ reader.addPackage( identifier, readZip( bytes, { budget: reader.budget } ) );
189
191
  const root = await load( await reader.packageRoot( identifier ) );
190
192
  layers.set( identifier, root );
191
193
  return root;
192
194
 
193
195
  }
194
196
 
195
- const layer = await parseLayer( bytes, identifier );
197
+ reader.budget.check();
198
+ const layer = await parseLayer( bytes, identifier, reader.budget );
196
199
  layers.set( identifier, layer );
197
200
 
198
201
  const pending = [];
@@ -216,6 +219,7 @@ async function loadLayers( reader, rootIdentifier ) {
216
219
 
217
220
  } catch ( e ) {
218
221
 
222
+ rethrowFatal( e, reader.budget );
219
223
  console.warn( `usd-gltf: ${ e.message }` );
220
224
 
221
225
  }
@@ -862,16 +866,16 @@ export class Stage {
862
866
  /**
863
867
  * Opens and composes a stage.
864
868
  * @param {Uint8Array} bytes usda, usdc or usdz
865
- * @param {{ identifier?: string, resolver?: ( identifier: string ) => Promise<Uint8Array>|Uint8Array, variants?: object }} [options]
869
+ * @param {{ identifier?: string, resolver?: ( identifier: string ) => Promise<Uint8Array>|Uint8Array, variants?: object, budget?: import('../limits.js').Budget }} [options]
866
870
  * @returns {Promise<Stage>}
867
871
  */
868
- export async function openStage( bytes, { identifier, resolver = null, variants = {} } = {} ) {
872
+ export async function openStage( bytes, { identifier, resolver = null, variants = {}, budget = new Budget() } = {} ) {
869
873
 
870
874
  const format = detectLayerFormat( bytes );
871
875
  if ( ! format ) throw new Error( 'usd-gltf: input is not usda, usdc or usdz' );
872
876
 
873
877
  identifier ??= 'root.' + format;
874
- const reader = new AssetReader( resolver );
878
+ const reader = new AssetReader( resolver, budget );
875
879
  reader.files.set( identifier, bytes );
876
880
 
877
881
  const { root, layers } = await loadLayers( reader, identifier );
@@ -357,11 +357,26 @@ function writePrim( out, spec, indent ) {
357
357
 
358
358
  /**
359
359
  * @param {import('../model.js').Layer} layer
360
+ * @param {{ budget?: import('../../limits.js').Budget }} [options] its output limit is checked as lines are written
360
361
  * @returns {string}
361
362
  */
362
- export function writeUSDA( layer ) {
363
+ export function writeUSDA( layer, { budget = null } = {} ) {
363
364
 
364
365
  const out = [ '#usda 1.0' ];
366
+
367
+ if ( budget ) {
368
+
369
+ let size = 0;
370
+ const push = out.push;
371
+ out.push = function ( ...lines ) {
372
+
373
+ for ( const line of lines ) size += line.length + 1;
374
+ budget.output( size, 'usda' );
375
+ return push.apply( this, lines );
376
+
377
+ };
378
+
379
+ }
365
380
  const meta = formatMetadataEntries( layer.metadata, ' ', 'layer' );
366
381
  if ( meta.length ) out.push( '(', ...meta, ')' );
367
382
  out.push( '' );
@@ -4,6 +4,7 @@
4
4
  import { AssetPath, DICT_TYPES, Layer, ListOp, PrimSpec, PropertySpec, Reference } from '../model.js';
5
5
  import { decompressFast } from './lz4.js';
6
6
  import { decompressInts32, decompressInts64 } from './intcompression.js';
7
+ import { Budget, checkSize } from '../../limits.js';
7
8
  import { PERMISSIONS, POD, SCALAR_SIZE, SPEC, SPECIFIERS, TYPE, TYPE_NAMES, halfArrayToFloat32, halfToFloat, isMatrixType, isVecType } from './types.js';
8
9
 
9
10
  const IS_ARRAY = 0x80000000;
@@ -42,9 +43,10 @@ const V0_8_0 = version( 0, 8, 0 );
42
43
 
43
44
  class CrateReader {
44
45
 
45
- constructor( bytes ) {
46
+ constructor( bytes, budget = new Budget() ) {
46
47
 
47
48
  this.bytes = bytes;
49
+ this.budget = budget;
48
50
  this.view = new DataView( bytes.buffer, bytes.byteOffset, bytes.byteLength );
49
51
  this.pos = 0;
50
52
  this.timesCache = new Map();
@@ -136,7 +138,7 @@ class CrateReader {
136
138
 
137
139
  take( n ) {
138
140
 
139
- if ( this.pos + n > this.bytes.length ) throw new Error( 'usd-gltf: truncated usdc file' );
141
+ if ( ! Number.isSafeInteger( n ) || n < 0 || this.pos + n > this.bytes.length ) throw new Error( 'usd-gltf: truncated usdc file' );
140
142
  const b = this.bytes.subarray( this.pos, this.pos + n );
141
143
  this.pos += n;
142
144
  return b;
@@ -145,8 +147,37 @@ class CrateReader {
145
147
 
146
148
  compressedInts( n ) {
147
149
 
148
- const size = this.u64();
149
- return decompressInts32( this.take( size ), n );
150
+ const data = this.take( this.u64() );
151
+ this.decodedInts( n, 4, data );
152
+ return decompressInts32( data, n );
153
+
154
+ }
155
+
156
+ // Sizes come from the file, so they are checked before allocating. Compressed integers take at
157
+ // least 2 bits each before LZ4, which shrinks data at most 255 times.
158
+ decodedInts( n, size, data ) {
159
+
160
+ checkSize( n * size, 'usdc' );
161
+ if ( n > data.length * 1024 + 64 ) throw new Error( 'usd-gltf: corrupt compressed integers in usdc file' );
162
+ this.budget.decode( n * size * 2, 'usdc' ); // the encoded buffer and the decoded integers
163
+
164
+ }
165
+
166
+ lz4( data, size ) {
167
+
168
+ checkSize( size, 'usdc' );
169
+ if ( size > data.length * 256 + 64 ) throw new Error( 'usd-gltf: corrupt compressed data in usdc file' );
170
+ this.budget.decode( size, 'usdc' );
171
+ return decompressFast( data, size );
172
+
173
+ }
174
+
175
+ // count fixed size records at this.pos, which must all be in the file
176
+ records( count, size ) {
177
+
178
+ checkSize( count * size, 'usdc' );
179
+ if ( this.pos + count * size > this.bytes.length ) throw new Error( 'usd-gltf: truncated usdc file' );
180
+ this.budget.decode( count * size, 'usdc' );
150
181
 
151
182
  }
152
183
 
@@ -176,7 +207,7 @@ class CrateReader {
176
207
 
177
208
  const uncompressedSize = this.u64();
178
209
  const compressedSize = this.u64();
179
- chars = decompressFast( this.take( compressedSize ), uncompressedSize );
210
+ chars = this.lz4( this.take( compressedSize ), uncompressedSize );
180
211
 
181
212
  }
182
213
 
@@ -213,6 +244,7 @@ class CrateReader {
213
244
 
214
245
  if ( this.version < V0_4_0 ) {
215
246
 
247
+ this.records( count, 16 );
216
248
  this.fieldTokens = new Uint32Array( count );
217
249
  this.fieldReps = new Uint32Array( count * 2 );
218
250
 
@@ -229,7 +261,7 @@ class CrateReader {
229
261
 
230
262
  this.fieldTokens = new Uint32Array( this.compressedInts( count ).buffer );
231
263
  const repsSize = this.u64();
232
- const reps = decompressFast( this.take( repsSize ), count * 8 );
264
+ const reps = this.lz4( this.take( repsSize ), count * 8 );
233
265
  this.fieldReps = new Uint32Array( reps.buffer, 0, count * 2 );
234
266
 
235
267
  }
@@ -245,6 +277,7 @@ class CrateReader {
245
277
 
246
278
  if ( this.version < V0_4_0 ) {
247
279
 
280
+ this.records( count, 4 );
248
281
  this.fieldSets = new Uint32Array( count );
249
282
  for ( let i = 0; i < count; i ++ ) this.fieldSets[ i ] = this.u32();
250
283
 
@@ -263,16 +296,27 @@ class CrateReader {
263
296
  if ( ! this.section( 'PATHS' ) ) return;
264
297
 
265
298
  const numPaths = this.u64();
266
- this.paths = new Array( numPaths ).fill( '' );
267
- this.pathInfo = new Array( numPaths ).fill( null );
299
+ const allocate = () => {
300
+
301
+ this.budget.decode( numPaths * 16, 'usdc' );
302
+ this.paths = new Array( numPaths ).fill( '' );
303
+ this.pathInfo = new Array( numPaths ).fill( null );
304
+
305
+ };
268
306
 
269
307
  if ( this.version < V0_4_0 ) {
270
308
 
271
- this.readPathTree( this.version === version( 0, 0, 1 ) ? 16 : 12 );
309
+ const entry = this.version === version( 0, 0, 1 ) ? 16 : 12;
310
+ this.records( numPaths, entry ); // a tree entry per path
311
+ allocate();
312
+ this.readPathTree( entry );
272
313
 
273
314
  } else {
274
315
 
275
316
  const n = this.u64();
317
+ // the tree holds every path, sometimes leaving out an unused first slot
318
+ if ( ! Number.isSafeInteger( numPaths ) || numPaths > n + 1 ) throw new Error( 'usd-gltf: corrupt path table in usdc file' );
319
+ allocate();
276
320
  const pathIndexes = this.compressedInts( n );
277
321
  const elementTokens = this.compressedInts( n );
278
322
  const jumps = this.compressedInts( n );
@@ -405,6 +449,7 @@ class CrateReader {
405
449
  if ( this.version < V0_4_0 ) {
406
450
 
407
451
  const pad = this.version === version( 0, 0, 1 ) ? 4 : 0;
452
+ this.records( count, 12 + pad );
408
453
  this.specPaths = new Uint32Array( count );
409
454
  this.specFieldSets = new Uint32Array( count );
410
455
  this.specTypes = new Uint32Array( count );
@@ -591,6 +636,7 @@ class CrateReader {
591
636
  readVector( item ) {
592
637
 
593
638
  const n = this.u64();
639
+ this.records( n, 1 ); // every item takes at least a byte
594
640
  const out = new Array( n );
595
641
  for ( let i = 0; i < n; i ++ ) out[ i ] = item();
596
642
  return out;
@@ -731,8 +777,8 @@ class CrateReader {
731
777
 
732
778
  if ( compressed && pod[ 1 ] === 1 && n >= MIN_COMPRESSED_ARRAY_SIZE && ( kind === 'i32' || kind === 'u32' || kind === 'i64' || kind === 'u64' ) && this.version >= V0_5_0 ) {
733
779
 
734
- const size = this.u64();
735
- const data = this.take( size );
780
+ const data = this.take( this.u64() );
781
+ this.decodedInts( n, kind === 'i32' || kind === 'u32' ? 4 : 8, data );
736
782
  if ( kind === 'i32' ) out = decompressInts32( data, n );
737
783
  else if ( kind === 'u32' ) out = new Uint32Array( decompressInts32( data, n ).buffer );
738
784
  else out = decompressInts64( data, n, kind === 'u64' );
@@ -769,6 +815,7 @@ class CrateReader {
769
815
 
770
816
  readArrayItems( n, item ) {
771
817
 
818
+ this.records( n, 4 );
772
819
  const out = new Array( n );
773
820
  for ( let i = 0; i < n; i ++ ) out[ i ] = item();
774
821
  return out;
@@ -778,6 +825,7 @@ class CrateReader {
778
825
  readRawArray( kind, count ) {
779
826
 
780
827
  const size = SCALAR_SIZE[ kind ];
828
+ this.records( count, kind === 'f16' || kind === 'i64' || kind === 'u64' ? size * 3 : size ); // and any widened copy
781
829
  const bytes = this.take( count * size ).slice(); // copy, also aligns
782
830
 
783
831
  switch ( kind ) {
@@ -812,6 +860,8 @@ class CrateReader {
812
860
  readCompressedFloats( kind, n ) {
813
861
 
814
862
  const code = this.u8();
863
+ checkSize( n * 8, 'usdc' );
864
+ this.budget.decode( n * ( kind === 'f64' ? 8 : 4 ), 'usdc' );
815
865
  const out = typedArrayFor( kind, n );
816
866
 
817
867
  if ( code === 105 ) { // 'i': integers
@@ -912,11 +962,11 @@ function payloadListOp( value ) {
912
962
  * @param {string} [identifier]
913
963
  * @returns {Layer}
914
964
  */
915
- export function readUSDC( data, identifier = 'anon.usdc' ) {
965
+ export function readUSDC( data, identifier = 'anon.usdc', { budget = new Budget() } = {} ) {
916
966
 
917
967
  // a plain view, so subarray().slice() copies even for a Node Buffer
918
968
  const bytes = ArrayBuffer.isView( data ) ? new Uint8Array( data.buffer, data.byteOffset, data.byteLength ) : new Uint8Array( data );
919
- const crate = new CrateReader( bytes );
969
+ const crate = new CrateReader( bytes, budget );
920
970
  const layer = new Layer( identifier );
921
971
 
922
972
  const specs = new Map(); // path index -> { type, object, order }
@@ -4,6 +4,7 @@
4
4
  import { AssetPath, DICT_TYPES, ListOp, Reference } from '../model.js';
5
5
  import { compressFast } from './lz4.js';
6
6
  import { compressInts32 } from './intcompression.js';
7
+ import { Budget } from '../../limits.js';
7
8
  import { PERMISSIONS, POD, SPEC, SPECIFIERS, TYPE, crateTypeFor, floatToHalf, halfToFloat, isMatrixType, isVecType } from './types.js';
8
9
 
9
10
  const IS_ARRAY = 0x80000000;
@@ -148,8 +149,9 @@ function pathElements( path ) {
148
149
 
149
150
  class CrateWriter {
150
151
 
151
- constructor() {
152
+ constructor( budget = new Budget() ) {
152
153
 
154
+ this.budget = budget;
153
155
  this.buf = new Uint8Array( 1 << 16 );
154
156
  this.view = new DataView( this.buf.buffer );
155
157
  this.pos = 88; // bootstrap, written last
@@ -173,6 +175,7 @@ class CrateWriter {
173
175
  this.fieldSetIndex = new Map();
174
176
  this.specs = [];
175
177
  this.timesCache = new Map();
178
+ this.arrayCache = new Map(); // hash -> [ { values, type, rep } ], so identical arrays are written once
176
179
 
177
180
  }
178
181
 
@@ -181,8 +184,10 @@ class CrateWriter {
181
184
  ensure( n ) {
182
185
 
183
186
  if ( this.pos + n <= this.buf.length ) return;
187
+ this.budget.output( this.pos + n, 'usdc' );
184
188
  let size = this.buf.length * 2;
185
189
  while ( size < this.pos + n ) size *= 2;
190
+ size = Math.min( size, Math.max( this.pos + n, this.budget.maxOutputBytes ) );
186
191
  const buf = new Uint8Array( size );
187
192
  buf.set( this.buf );
188
193
  this.buf = buf;
@@ -337,6 +342,7 @@ class CrateWriter {
337
342
 
338
343
  addSpec( path, specType, fields ) {
339
344
 
345
+ this.budget.check();
340
346
  const indexes = [];
341
347
 
342
348
  for ( const [ name, [ lo, hi ] ] of fields ) {
@@ -690,8 +696,24 @@ class CrateWriter {
690
696
 
691
697
  }
692
698
 
699
+ // Identical numeric arrays share one copy in the file, as OpenUSD's crate writer does.
693
700
  packArray( values, type ) {
694
701
 
702
+ if ( ! POD[ type ] || values.length < MIN_COMPRESSED_ARRAY_SIZE ) return this.packArrayData( values, type );
703
+
704
+ const key = type + ':' + values.length + ':' + hashValues( values );
705
+ const entries = this.arrayCache.get( key ) || this.arrayCache.set( key, [] ).get( key );
706
+ const hit = entries.find( ( e ) => sameValues( e.values, values ) );
707
+ if ( hit ) return hit.rep;
708
+
709
+ const rep = this.packArrayData( values, type );
710
+ entries.push( { values, rep } );
711
+ return rep;
712
+
713
+ }
714
+
715
+ packArrayData( values, type ) {
716
+
695
717
  const flags = ( IS_ARRAY | ( type << 16 ) ) >>> 0;
696
718
  const pod = POD[ type ];
697
719
  const total = values.length;
@@ -949,6 +971,33 @@ class CrateWriter {
949
971
 
950
972
  }
951
973
 
974
+ const F64 = new Float64Array( 1 );
975
+ const F64_WORDS = new Int32Array( F64.buffer );
976
+
977
+ function hashValues( values ) {
978
+
979
+ let h = 0x811c9dc5;
980
+
981
+ for ( let i = 0; i < values.length; i ++ ) {
982
+
983
+ F64[ 0 ] = values[ i ];
984
+ h = Math.imul( h ^ F64_WORDS[ 0 ], 16777619 );
985
+ h = Math.imul( h ^ F64_WORDS[ 1 ], 16777619 );
986
+
987
+ }
988
+
989
+ return h >>> 0;
990
+
991
+ }
992
+
993
+ function sameValues( a, b ) {
994
+
995
+ if ( a.length !== b.length ) return false;
996
+ for ( let i = 0; i < a.length; i ++ ) if ( ! Object.is( a[ i ], b[ i ] ) ) return false;
997
+ return true;
998
+
999
+ }
1000
+
952
1001
  function isInt8( v ) {
953
1002
 
954
1003
  return Number.isInteger( v ) && v >= - 128 && v <= 127 && ! Object.is( v, - 0 );
@@ -1087,11 +1136,12 @@ function writeProperty( w, prop, path ) {
1087
1136
  /**
1088
1137
  * Writes a Layer as a binary USD crate file.
1089
1138
  * @param {import('../model.js').Layer} layer
1139
+ * @param {{ budget?: import('../../limits.js').Budget }} [options]
1090
1140
  * @returns {Uint8Array}
1091
1141
  */
1092
- export function writeUSDC( layer ) {
1142
+ export function writeUSDC( layer, { budget = new Budget() } = {} ) {
1093
1143
 
1094
- const w = new CrateWriter();
1144
+ const w = new CrateWriter( budget );
1095
1145
  const root = layer.root;
1096
1146
 
1097
1147
  const fields = metadataFields( w, layer.metadata );
package/src/usd/usdz.js CHANGED
@@ -3,6 +3,7 @@
3
3
 
4
4
  import { crc32 } from '../util/crc32.js';
5
5
  import { inflate } from '../image/inflate.js';
6
+ import { Budget } from '../limits.js';
6
7
 
7
8
  const LOCAL = 0x04034b50;
8
9
  const CENTRAL = 0x02014b50;
@@ -12,9 +13,10 @@ const ALIGN = 64;
12
13
  /**
13
14
  * Lists the files in a zip, in archive order.
14
15
  * @param {Uint8Array} bytes
16
+ * @param {{ budget?: import('../limits.js').Budget }} [options]
15
17
  * @returns {Map<string, Uint8Array>}
16
18
  */
17
- export function readZip( bytes ) {
19
+ export function readZip( bytes, { budget = new Budget() } = {} ) {
18
20
 
19
21
  const view = new DataView( bytes.buffer, bytes.byteOffset, bytes.byteLength );
20
22
  const files = new Map();
@@ -33,6 +35,7 @@ export function readZip( bytes ) {
33
35
 
34
36
  const method = view.getUint16( offset + 10, true );
35
37
  const compressedSize = view.getUint32( offset + 20, true );
38
+ const size = view.getUint32( offset + 24, true );
36
39
  const nameLength = view.getUint16( offset + 28, true );
37
40
  const extraLength = view.getUint16( offset + 30, true );
38
41
  const commentLength = view.getUint16( offset + 32, true );
@@ -40,12 +43,13 @@ export function readZip( bytes ) {
40
43
  const name = decoder.decode( bytes.subarray( offset + 46, offset + 46 + nameLength ) );
41
44
 
42
45
  const dataStart = localOffset + 30 + view.getUint16( localOffset + 26, true ) + view.getUint16( localOffset + 28, true );
46
+ if ( dataStart + compressedSize > bytes.length ) throw new Error( 'usd-gltf: truncated zip archive' );
43
47
  const data = bytes.subarray( dataStart, dataStart + compressedSize );
44
48
 
45
49
  if ( ! name.endsWith( '/' ) ) {
46
50
 
47
51
  if ( method === 0 ) files.set( name, data );
48
- else if ( method === 8 ) files.set( name, inflate( data ) );
52
+ else if ( method === 8 ) { budget.decode( size, 'zip' ); files.set( name, inflate( data, size, size ) ); }
49
53
  else throw new Error( `usd-gltf: unsupported zip compression ${ method } for ${ name }` );
50
54
 
51
55
  }
@@ -61,9 +65,10 @@ export function readZip( bytes ) {
61
65
  /**
62
66
  * Writes a USDZ package. The first entry must be the root layer.
63
67
  * @param {Array<{ name: string, data: Uint8Array }>} entries
68
+ * @param {{ budget?: import('../limits.js').Budget }} [options]
64
69
  * @returns {Uint8Array}
65
70
  */
66
- export function writeUSDZ( entries ) {
71
+ export function writeUSDZ( entries, { budget = new Budget() } = {} ) {
67
72
 
68
73
  const encoder = new TextEncoder();
69
74
  const locals = [];
@@ -127,6 +132,7 @@ export function writeUSDZ( entries ) {
127
132
  ev.setUint32( 12, centralSize, true );
128
133
  ev.setUint32( 16, offset, true );
129
134
 
135
+ budget.output( offset + centralSize + 22, 'usdz' );
130
136
  const out = new Uint8Array( offset + centralSize + 22 );
131
137
  let p = 0;
132
138
  for ( const part of [ ...locals, ...centrals, end ] ) {