openusd 0.8.0

Rust native USD library
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
//! USDZ archive format reader and writer.
//!
//! USDZ is a ZIP archive containing USD layer files (and optional adjacent
//! resources such as textures). Per the specification, archived files are
//! stored uncompressed (STORED, method 0) and aligned to a 64-byte boundary
//! so the contained data can be consumed in place without extraction.
//!
//! The reader does consume it in place: [`Archive::entry`] serves a stored
//! entry as a view of the package's bytes when the asset shares them, and
//! refuses a compressed or encrypted entry as C++ does. A view is served
//! without checking the entry's CRC32, again as C++ serves a mapped package;
//! [`Archive::verify`] checks one on request, and the bounded read a package
//! without shared bytes falls back to checks it on the way. The `deflate`
//! feature of the `zip` crate stays for `usd_utils` package rebuilding,
//! which copies entries through the ZIP reader.

mod package;
mod reader;
mod writer;

pub use package::{Package, PackageError, PackageStats, open_package, package_contains, package_entry, package_stats};
pub use reader::Archive;
pub(crate) use reader::is_layer_name;
pub use writer::ArchiveWriter;

use std::io::{self, Cursor};
use std::str;

use crate::{ar, sdf, tf, usda, usdc};

/// Error reading or writing a `.usdz` package ([`Archive`] /
/// [`ArchiveWriter`]).
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum ArchiveError {
    /// Byte I/O against the package failed.
    #[error(transparent)]
    Io(#[from] io::Error),

    /// The ZIP layer failed while reading or writing the archive.
    #[error(transparent)]
    Zip(#[from] zip::result::ZipError),

    /// A named entry could not be read from or written to the archive.
    #[error("failed to access USDZ entry {name:?}")]
    Entry {
        /// The archive-relative entry name.
        name: String,
        /// The underlying failure.
        #[source]
        source: Box<ArchiveError>,
    },

    /// The archive holds no USD layer to serve as the package's default.
    #[error("no USD layer found in USDZ archive")]
    NoDefaultLayer,

    /// The writer refuses an unsafe or non-portable entry name.
    #[error("USDZ entry name {name:?} {reason}")]
    InvalidEntryName {
        /// The offending entry name.
        name: String,
        /// What the name violates.
        reason: &'static str,
    },

    /// A packaged crate layer failed to decode.
    #[error(transparent)]
    Read(#[from] usdc::ReadError),

    /// A packaged text layer failed to parse.
    #[error(transparent)]
    Parse(#[from] usda::ParseError),

    /// A packaged text layer is not valid UTF-8.
    #[error("file {name:?} is not valid UTF-8")]
    Utf8 {
        /// The archive-relative entry name.
        name: String,
        /// The underlying UTF-8 failure.
        #[source]
        source: str::Utf8Error,
    },

    /// An entry is compressed, which the USDZ specification forbids: only a
    /// stored entry can be served in place.
    #[error("USDZ entry {name:?} is {method} compressed; only stored entries are read")]
    Compressed {
        /// The archive-relative entry name.
        name: String,
        /// The compression the entry uses.
        method: zip::CompressionMethod,
    },

    /// An entry is encrypted, which the USDZ specification forbids.
    #[error("USDZ entry {name:?} is encrypted; only plain entries are read")]
    Encrypted {
        /// The archive-relative entry name.
        name: String,
    },
}

impl ArchiveError {
    /// Wraps a failure with the archive-relative entry it struck.
    pub(crate) fn entry(name: impl Into<String>, source: impl Into<ArchiveError>) -> Self {
        Self::Entry {
            name: name.into(),
            source: Box::new(source.into()),
        }
    }

    /// The [`io::ErrorKind`] at the heart of a write failure, seen through
    /// the entry and ZIP wrappers, or `None` when the failure is about the
    /// data rather than the destination. Meaningful only where the sink is
    /// real storage — the write seam; on the read side the package is already
    /// in hand, so a nested I/O failure there means truncated content.
    fn io_kind(&self) -> Option<io::ErrorKind> {
        match self {
            Self::Io(error) | Self::Zip(zip::result::ZipError::Io(error)) => Some(error.kind()),
            Self::Entry { source, .. } => source.io_kind(),
            _ => None,
        }
    }
}

/// Archive package format (`.usdz`) as an [`sdf::FileFormat`], wrapping
/// [`Archive`] and [`ArchiveWriter`]. Writing wraps a single crate-encoded
/// layer.
pub struct UsdzFileFormat;

/// Name of the single inner crate entry written into a `.usdz` package.
///
/// `write` only sees the sink, not the destination filename, so the entry name
/// is fixed; reading back is name-agnostic ([`Archive::read_first_layer`] takes
/// the first entry).
const USDZ_LAYER_NAME: &str = "layer.usdc";

impl sdf::FileFormat for UsdzFileFormat {
    fn format_id(&self) -> tf::Token {
        tf::Token::new("usdz")
    }

    fn extensions(&self) -> &[&str] {
        &["usdz"]
    }

    fn caps(&self) -> sdf::FileFormatCaps {
        // Writable as a fresh single-layer archive (`export`), but not editable
        // in place (`save`): a loaded package's other assets — textures, sibling
        // layers — are not held by the layer, so overwriting it would drop them.
        sdf::FileFormatCaps::READ | sdf::FileFormatCaps::WRITE
    }

    fn is_package(&self) -> bool {
        true
    }

    fn resolve_layer(&self, resolver: &dyn ar::Resolver, resolved: &ar::ResolvedPath) -> Option<ar::ResolvedPath> {
        // A package is anchored to its default (first) packaged layer. A package
        // nested in another (`pkg.usdz[inner.usdz]`) is anchored inside the
        // innermost bracket, `pkg.usdz[inner.usdz[first.usdc]]`.
        let package = resolved.to_string_lossy();
        // A package that cannot be opened, or that lists no default layer, falls
        // back to the bare package path so `read` surfaces the precise zip/parse
        // error, rather than being demoted to an unresolved (missing) asset.
        //
        // Only the central directory is read to list the default layer,
        // whatever the asset, and the package is the one the cache scope
        // holds for the entry reads that follow.
        let first = open_package(resolver, resolved)
            .ok()
            .and_then(|package| package.with(|archive| archive.first_layer_name()));
        Some(match first {
            Some(first) => ar::ResolvedPath::new(ar::nest_packaged_path(&package, &first)),
            None => resolved.clone(),
        })
    }

    fn read_bytes(&self, bytes: ar::AssetBuffer, _source_name: &str) -> Result<sdf::LayerData, sdf::FormatError> {
        // A bare package has no named entry, so read its first (default) layer.
        //
        // Every failure here is a decode: the bytes are already in hand, so
        // even an I/O error comes from the cursor reading them and means the
        // package is truncated or corrupt.
        Archive::from_bytes(bytes)
            .and_then(|mut archive| archive.read_first_layer())
            .map_err(|error| sdf::FormatError::Decode(Box::new(error)))
    }

    /// Reads the default layer of the package at `resolved` from the
    /// package [`open_package`] gives, which a cache scope shares with the
    /// lookups around this read.
    fn read(
        &self,
        resolver: &dyn ar::Resolver,
        resolved: &ar::ResolvedPath,
    ) -> Result<sdf::LayerData, sdf::FormatError> {
        // Failing to open the package's file is an I/O failure. Any failure
        // past that, an I/O error from reading the directory among them,
        // means the bytes are not a whole package.
        let package = open_package(resolver, resolved).map_err(|error| match error.archive_error() {
            ArchiveError::Io(_) => sdf::FormatError::Io(error.into()),
            _ => sdf::FormatError::Decode(Box::new(error)),
        })?;
        package
            .with(|archive| archive.read_first_layer())
            .map_err(|error| sdf::FormatError::Decode(Box::new(error)))
    }

    fn write(&self, data: &dyn sdf::AbstractData, sink: &mut dyn sdf::WriteSeek) -> Result<(), sdf::FormatError> {
        // Package-write failures map onto the format seam: a failure that is
        // byte I/O at heart — even wrapped in an entry or ZIP layer — stays
        // `Io`, keeping its kind and carrying the archive error as its
        // source; anything else is an encode failure.
        let encode = |error: ArchiveError| match error.io_kind() {
            Some(kind) => sdf::FormatError::Io(io::Error::new(kind, error)),
            None => sdf::FormatError::Encode {
                reason: tf::error_chain(&error).into(),
            },
        };
        let mut buf = Vec::new();
        usdc::CrateWriter::write(data, &mut Cursor::new(&mut buf))?;
        let mut archive = ArchiveWriter::new(sink);
        archive.add_layer(USDZ_LAYER_NAME, &buf).map_err(encode)?;
        archive.finish().map_err(encode)?;
        Ok(())
    }
}

#[cfg(test)]
pub(crate) mod tests {
    use super::*;
    use crate::Result;
    use crate::ar::Resolver;
    use crate::ar::tests::TestResolver;
    use crate::sdf::FileFormat;
    use crate::usd::{PrimPredicate, Stage, TimeCode};
    use std::fs;
    use std::io::Write;
    use std::sync::Arc;
    use std::sync::atomic::{AtomicUsize, Ordering};

    /// A resolver whose assets exist but cannot be opened, standing in for a
    /// storage failure underneath a resolved package.
    fn failing_resolver() -> impl ar::Resolver {
        TestResolver(|| -> io::Result<Box<dyn ar::Asset>> { Err(io::Error::from(io::ErrorKind::PermissionDenied)) })
    }

    /// An in-memory asset counting the bytes read from it.
    struct CountingAsset {
        inner: Cursor<Vec<u8>>,
        read: Arc<AtomicUsize>,
    }

    impl io::Read for CountingAsset {
        fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
            let count = self.inner.read(buf)?;
            self.read.fetch_add(count, Ordering::Relaxed);
            Ok(count)
        }
    }

    impl io::Seek for CountingAsset {
        fn seek(&mut self, pos: io::SeekFrom) -> io::Result<u64> {
            self.inner.seek(pos)
        }
    }

    impl ar::Asset for CountingAsset {
        fn size(&self) -> io::Result<u64> {
            self.inner.size()
        }
    }

    /// A sink whose first write fails, standing in for a storage failure
    /// underneath the archive writer. Later writes are swallowed, so the
    /// `ZipWriter` drop can finalize quietly after the failure aborts the
    /// caller (the zip crate warns on stderr when that finalize fails too).
    struct FailingSink {
        tripped: bool,
    }

    impl io::Write for FailingSink {
        fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
            if self.tripped {
                return Ok(buf.len());
            }
            self.tripped = true;
            Err(io::Error::from(io::ErrorKind::BrokenPipe))
        }

        fn flush(&mut self) -> io::Result<()> {
            Ok(())
        }
    }

    impl io::Seek for FailingSink {
        fn seek(&mut self, _pos: io::SeekFrom) -> io::Result<u64> {
            Ok(0)
        }
    }

    #[test]
    fn read_io_stays_io() {
        let Err(error) = UsdzFileFormat.read(&failing_resolver(), &ar::ResolvedPath::new("pkg.usdz")) else {
            panic!("asset open fails");
        };
        let sdf::FormatError::Io(error) = error else {
            panic!("a storage failure must stay I/O, got: {error}");
        };
        assert_eq!(error.kind(), io::ErrorKind::PermissionDenied);
    }

    #[test]
    fn write_io_stays_io() {
        let mut data = sdf::Data::new();
        data.create_spec(sdf::Path::abs_root(), sdf::SpecType::PseudoRoot);
        let mut sink = FailingSink { tripped: false };
        let error = UsdzFileFormat.write(&data, &mut sink).expect_err("sink writes fail");
        let sdf::FormatError::Io(error) = error else {
            panic!("a storage failure must stay I/O, got: {error}");
        };
        assert_eq!(error.kind(), io::ErrorKind::BrokenPipe);
    }

    /// A `.usdz` whose root layer references another layer *inside the same
    /// archive*. The reference (`@./inner.usda@`) must resolve in-package —
    /// not against the host filesystem — for the inner opinion to compose onto
    /// the root prim. Exercises the full package-relative resolution path
    /// (bare-package anchoring + `create_identifier` + inner-layer read).
    #[test]
    fn resolves_packaged_reference() -> Result<()> {
        let root =
            b"#usda 1.0\n(defaultPrim = \"World\")\ndef \"World\" (prepend references = @./inner.usda@</Inner>) {}\n";
        let inner = b"#usda 1.0\ndef \"Inner\" { custom int probe = 42 }\n";

        let dir = tempfile::tempdir()?;
        let path = dir.path().join("pkg.usdz");
        let mut writer = ArchiveWriter::create(&path)?;
        writer.add_layer("root.usda", root)?; // first entry is the root layer
        writer.add_layer("inner.usda", inner)?;
        writer.finish()?;

        let stage = Stage::open(path.to_str().unwrap())?;
        assert_eq!(
            stage
                .attribute("/World.probe")?
                .get_at::<sdf::Value>(TimeCode::new(0.0))?,
            Some(sdf::Value::Int(42)),
            "reference to a layer inside the package should compose"
        );
        Ok(())
    }

    /// Anchoring a bare package to its default layer reads the archive's
    /// central directory, not the packaged entries.
    #[test]
    fn default_layer_central_directory() -> Result<()> {
        let mut writer = ArchiveWriter::new(Cursor::new(Vec::new()));
        writer.add_layer("root.usda", b"#usda 1.0\n")?;
        writer.add_layer("texture.bin", &vec![0; 1 << 20])?;
        let package = writer.finish()?.into_inner();
        let size = package.len();
        let read = Arc::new(AtomicUsize::new(0));
        let resolver = TestResolver({
            let read = read.clone();
            move || -> io::Result<Box<dyn ar::Asset>> {
                Ok(Box::new(CountingAsset {
                    inner: Cursor::new(package.clone()),
                    read: read.clone(),
                }))
            }
        });

        let resolved = UsdzFileFormat.resolve_layer(&resolver, &ar::ResolvedPath::new("pkg.usdz"));
        assert_eq!(resolved, Some(ar::ResolvedPath::new("pkg.usdz[root.usda]")));
        let read = read.load(Ordering::Relaxed);
        assert!(read < size / 4, "read {read} of {size} bytes");
        Ok(())
    }

    /// A package read from an asset that shares its bytes decodes its default
    /// layer from the shared bytes.
    #[test]
    fn shared_buffer_read() -> Result<()> {
        let mut writer = ArchiveWriter::new(Cursor::new(Vec::new()));
        writer.add_layer("root.usda", b"#usda 1.0\ndef \"Root\" {}\n")?;
        let package: Arc<[u8]> = writer.finish()?.into_inner().into();
        let resolver = TestResolver({
            let package = ar::AssetBuffer::from(package.clone());
            move || -> io::Result<Box<dyn ar::Asset>> { Ok(Box::new(Cursor::new(package.clone()))) }
        });

        let data = UsdzFileFormat.read(&resolver, &ar::ResolvedPath::new("pkg.usdz"))?;
        assert_eq!(data.spec_type(&sdf::path("/Root").unwrap()), Some(sdf::SpecType::Prim));
        Ok(())
    }

    /// A package over bytes in hand serves an entry as a view into them.
    #[test]
    fn entry_is_view() -> Result<()> {
        let layer = b"#usda 1.0\ndef \"Root\" {}\n";
        let mut writer = ArchiveWriter::new(Cursor::new(Vec::new()));
        writer.add_layer("root.usda", layer)?;
        let package = writer.finish()?.into_inner();
        let bounds = package.as_ptr_range();

        let mut archive = Archive::from_bytes(package)?;
        let ar::AssetBuffer::Shared(view) = archive.entry("root.usda")? else {
            panic!("a package in hand serves views");
        };
        assert!(
            bounds.start <= view.as_ptr() && view.as_ptr_range().end <= bounds.end,
            "the view lies inside the package"
        );
        assert_eq!(&*view, layer);
        Ok(())
    }

    /// A package over an asset with no shared bytes probes its directory
    /// alone and reads an entry's range through the ZIP reader: the
    /// directory, one local header and the entry, not the package.
    #[test]
    fn entry_bounded_read() -> Result<()> {
        let mut writer = ArchiveWriter::new(Cursor::new(Vec::new()));
        writer.add_layer("root.usda", b"#usda 1.0\n")?;
        writer.add_layer("texture.bin", &vec![0; 1 << 20])?;
        let package = writer.finish()?.into_inner();
        let size = package.len();
        let read = Arc::new(AtomicUsize::new(0));
        let mut archive = Archive::from_asset(Box::new(CountingAsset {
            inner: Cursor::new(package),
            read: read.clone(),
        }))?;

        assert!(archive.contains("root.usda"));
        assert!(archive.contains("texture.bin"));
        assert!(!archive.contains("missing.usda"));
        assert_eq!(archive.first_layer_name().as_deref(), Some("root.usda"));
        let probed = read.load(Ordering::Relaxed);
        assert!(probed < size / 4, "the probes read {probed} of {size} bytes");

        let ar::AssetBuffer::Owned(bytes) = archive.entry("root.usda")? else {
            panic!("an asset without shared bytes is read");
        };
        assert_eq!(bytes, b"#usda 1.0\n");
        let entry = read.load(Ordering::Relaxed) - probed;
        assert!(entry < 4096, "the entry read {entry} bytes");
        Ok(())
    }

    /// A compressed entry is refused, as C++ refuses it.
    #[test]
    fn compressed_entry_rejected() -> Result<()> {
        let mut zip = zip::ZipWriter::new(Cursor::new(Vec::new()));
        let deflated = zip::write::SimpleFileOptions::default().compression_method(zip::CompressionMethod::Deflated);
        zip.start_file("root.usda", deflated).map_err(ArchiveError::from)?;
        zip.write_all(b"#usda 1.0\n")?;
        let package = zip.finish().map_err(ArchiveError::from)?.into_inner();

        let error = Archive::from_bytes(package)?
            .entry("root.usda")
            .expect_err("a deflated entry is refused");
        assert!(matches!(error, ArchiveError::Compressed { .. }), "{error:?}");
        Ok(())
    }

    /// A view is served without a checksum check; `verify` reports the
    /// corruption, and so does the bounded read of a package without shared
    /// bytes.
    #[test]
    fn corrupt_entry() -> Result<()> {
        let layer = b"#usda 1.0\ndef \"Root\" {}\n";
        let mut writer = ArchiveWriter::new(Cursor::new(Vec::new()));
        writer.add_layer("root.usda", layer)?;
        let mut package = writer.finish()?.into_inner();
        let offset = package
            .windows(layer.len())
            .position(|window| window == layer)
            .expect("the stored entry");
        package[offset + 1] ^= 0xff;

        let mut archive = Archive::from_bytes(package.clone())?;
        assert_eq!(archive.entry("root.usda")?[1], layer[1] ^ 0xff);
        let error = archive.verify("root.usda").expect_err("the checksum no longer matches");
        assert!(format!("{error:?}").to_lowercase().contains("checksum"), "{error:?}");

        let mut archive = Archive::from_asset(Box::new(Cursor::new(package)))?;
        assert!(
            archive.entry("root.usda").is_err(),
            "a checked read reports the corruption"
        );
        Ok(())
    }

    /// A nested package's entry is a view into the outer package.
    #[test]
    fn nested_entry_is_view() -> Result<()> {
        let mut inner = ArchiveWriter::new(Cursor::new(Vec::new()));
        inner.add_layer("inner.usda", b"#usda 1.0\n")?;
        let inner = inner.finish()?.into_inner();
        let mut outer = ArchiveWriter::new(Cursor::new(Vec::new()));
        outer.add_layer("root.usda", b"#usda 1.0\n")?;
        outer.add_layer("inner.usdz", &inner)?;
        let outer = outer.finish()?.into_inner();
        let bounds = outer.as_ptr_range();

        let mut archive = Archive::from_bytes(outer)?;
        let mut nested = Archive::from_bytes(archive.entry("inner.usdz")?)?;
        let ar::AssetBuffer::Shared(view) = nested.entry("inner.usda")? else {
            panic!("a nested package serves views");
        };
        assert!(
            bounds.start <= view.as_ptr() && view.as_ptr_range().end <= bounds.end,
            "the view lies inside the outer package"
        );
        assert_eq!(&*view, b"#usda 1.0\n");
        Ok(())
    }

    /// A production package opens as a stage.
    #[test]
    fn stage_over_package() -> Result<()> {
        let path = concat!(
            env!("CARGO_WORKSPACE_DIR"),
            "vendor/usd-wg-assets/full_assets/CarbonFrameBike/CarbonFrameBike.usdz"
        );
        if fs::metadata(path).is_err() {
            eprintln!("Skipping stage_over_package: fixture not available at {path}");
            return Ok(());
        }
        let stage = Stage::open(path)?;
        let mut prims = 0;
        stage.traverse(PrimPredicate::DEFAULT, |_| prims += 1)?;
        assert!(prims > 100, "{prims} prims");
        Ok(())
    }

    /// A `.usdz` holding another `.usdz`, as C++ reads one: a reference to the
    /// inner package composes its default layer, the nested package path
    /// `outer.usdz[inner.usdz]` opens as a stage, and the raw archive reads the
    /// nested entry's default layer.
    #[test]
    fn resolves_nested_package() -> Result<()> {
        let mut inner = ArchiveWriter::new(Cursor::new(Vec::new()));
        inner.add_layer("inner.usda", b"#usda 1.0\ndef \"Inner\" { custom int probe = 42 }\n")?;
        let inner = inner.finish()?.into_inner();
        let root = b"#usda 1.0\ndef \"World\" (prepend references = @./inner.usdz@</Inner>) {}\n";

        let dir = tempfile::tempdir()?;
        let path = dir.path().join("outer.usdz");
        let mut writer = ArchiveWriter::create(&path)?;
        writer.add_layer("root.usda", root)?;
        writer.add_layer("inner.usdz", &inner)?;
        writer.finish()?;

        let probe = |stage: &Stage, attr: &str| -> Result<Option<sdf::Value>> {
            stage.attribute(attr)?.get_at::<sdf::Value>(TimeCode::new(0.0))
        };
        let stage = Stage::open(path.to_str().unwrap())?;
        assert_eq!(probe(&stage, "/World.probe")?, Some(sdf::Value::Int(42)));

        let nested = ar::join_package_relative_path(path.to_str().unwrap(), "inner.usdz");
        let stage = Stage::open(&nested)?;
        assert_eq!(probe(&stage, "/Inner.probe")?, Some(sdf::Value::Int(42)));

        let data = Archive::open(&path)?.read("inner.usdz")?;
        assert!(data.has_spec(&sdf::path("/Inner")?));

        // A missing entry at any bracket level is unresolved, not an entry
        // that fails to read.
        let resolver = ar::DefaultResolver::new();
        for missing in ["inner.usdz[missing.usda]", "missing.usdz[inner.usda]"] {
            let nested = ar::join_package_relative_path(path.to_str().unwrap(), missing);
            assert_eq!(resolver.resolve(&nested), None, "{nested}");
        }
        Ok(())
    }

    /// The bytes of a package holding `entries`, in order.
    pub(crate) fn package(entries: &[(&str, &[u8])]) -> Vec<u8> {
        let mut writer = ArchiveWriter::new(Cursor::new(Vec::new()));
        for (name, bytes) in entries {
            writer.add_layer(name, bytes).expect("a valid entry");
        }
        writer.finish().expect("a finished package").into_inner()
    }
}