usd-gltf 0.0.1 → 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.
@@ -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 ] ) {