Class: OpenUSD::Format::Usdz::Reader

Inherits:
Object
  • Object
show all
Defined in:
lib/openusd/format/usdz/reader.rb

Overview

Reads and validates the constrained ZIP representation used by USDZ.

Defined Under Namespace

Classes: Entry

Constant Summary collapse

PACKAGE_URI =

Virtual identifier for an entry inside a package.

/\A(.+\.usdz)\[([^\]]+)\]\z/i
LOCAL_SIGNATURE =

ZIP local-file-header signature.

0x04034B50
CENTRAL_SIGNATURE =

ZIP central-directory-entry signature.

0x02014B50
END_SIGNATURE =

ZIP end-of-central-directory signature.

0x06054B50

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(path) ⇒ Reader

Returns a new instance of Reader.



76
77
78
79
80
81
82
# File 'lib/openusd/format/usdz/reader.rb', line 76

def initialize(path)
  @path = File.expand_path(path)
  @archive = File.binread(@path)
  @entries = nil
rescue Errno::ENOENT
  raise PackageError, "package not found: #{path}"
end

Instance Attribute Details

#pathObject (readonly)

Returns the value of attribute path.



74
75
76
# File 'lib/openusd/format/usdz/reader.rb', line 74

def path
  @path
end

Class Method Details

.parse_layer(package_path, root) ⇒ Layer

Parse a validated package entry as USDA.

Returns:

Raises:



56
57
58
59
60
61
62
63
64
65
# File 'lib/openusd/format/usdz/reader.rb', line 56

def parse_layer(package_path, root)
  extension = File.extname(root.name).downcase
  raise NotSupportedError, "USDC root layers are not supported" if extension == ".usdc"
  raise PackageError, "first USDZ entry must be a native USD layer" unless %w[.usd .usda].include?(extension)

  layer = Format::Usda::Parser.parse(root.data.dup.force_encoding(Encoding::UTF_8),
                                     file: "#{package_path}[#{root.name}]")
  layer.identifier = "#{File.expand_path(package_path)}[#{root.name}]"
  layer
end

.parse_uri(uri) ⇒ Array(String, String)

Split a virtual package URI.

Returns:

  • (Array(String, String))

    package path and entry name

Raises:



47
48
49
50
51
52
# File 'lib/openusd/format/usdz/reader.rb', line 47

def parse_uri(uri)
  match = PACKAGE_URI.match(uri.to_s)
  raise PackageError, "invalid package URI: #{uri}" unless match

  [File.expand_path(match[1]), match[2]]
end

.read(path) ⇒ Layer

Read the default layer from a package.

Returns:

Raises:



26
27
28
29
30
31
32
# File 'lib/openusd/format/usdz/reader.rb', line 26

def read(path)
  package = new(path)
  root = package.entries.first
  raise PackageError, "USDZ package is empty" unless root

  parse_layer(package.path, root)
end

.read_uri(uri) ⇒ Layer

Read a layer identified by a virtual package URI.

Returns:

Raises:



36
37
38
39
40
41
42
43
# File 'lib/openusd/format/usdz/reader.rb', line 36

def read_uri(uri)
  package_path, entry_name = parse_uri(uri)
  package = new(package_path)
  entry = package.entry(entry_name)
  raise PackageError, "package entry not found: #{entry_name}" unless entry

  parse_layer(package.path, entry)
end

.unpack(path, destination:) ⇒ Array<String>

Extract a validated package.

Returns:

  • (Array<String>)

    extracted entry names



69
70
71
# File 'lib/openusd/format/usdz/reader.rb', line 69

def unpack(path, destination:)
  new(path).extract(destination)
end

Instance Method Details

#entriesArray<Entry>

Returns validated entries in archive order.

Returns:

  • (Array<Entry>)

    validated entries in archive order



85
86
87
# File 'lib/openusd/format/usdz/reader.rb', line 85

def entries
  @entries ||= parse_entries.freeze
end

#entry(name) ⇒ Entry?

Find an entry by its exact package path.

Returns:



91
92
93
# File 'lib/openusd/format/usdz/reader.rb', line 91

def entry(name)
  entries.find { |candidate| candidate.name == name.to_s }
end

#extract(destination) ⇒ Array<String>

Securely extract all entries below a destination.

Returns:

  • (Array<String>)

    extracted entry names



97
98
99
100
101
102
103
104
105
106
107
108
109
# File 'lib/openusd/format/usdz/reader.rb', line 97

def extract(destination)
  root = File.expand_path(destination)
  entries.each do |entry|
    output = File.expand_path(entry.name, root)
    unless output.start_with?("#{root}#{File::SEPARATOR}")
      raise PackageError, "unsafe package entry: #{entry.name.inspect}"
    end

    FileUtils.mkdir_p(File.dirname(output))
    File.binwrite(output, entry.data)
  end
  entries.map(&:name)
end