ruby3mf 0.2.5 → 0.2.6
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.
- checksums.yaml +4 -4
- data/.gitignore +13 -13
- data/.rspec +2 -2
- data/bin/batch.rb +3 -1
- data/bin/cli.rb +12 -12
- data/bin/console +14 -14
- data/bin/folder_test.sh +13 -13
- data/bin/suite_test.sh +41 -41
- data/lib/ruby3mf.rb +26 -26
- data/lib/ruby3mf/3MFcoreSpec_1.1.xsd.template +188 -188
- data/lib/ruby3mf/content_types.rb +77 -73
- data/lib/ruby3mf/document.rb +242 -242
- data/lib/ruby3mf/edge_list.rb +60 -60
- data/lib/ruby3mf/errors.yml +188 -188
- data/lib/ruby3mf/log3mf.rb +135 -135
- data/lib/ruby3mf/mesh_analyzer.rb +80 -80
- data/lib/ruby3mf/mesh_normal_analyzer.rb +218 -218
- data/lib/ruby3mf/model3mf.rb +117 -118
- data/lib/ruby3mf/relationships.rb +50 -50
- data/lib/ruby3mf/schema_files.rb +26 -26
- data/lib/ruby3mf/texture3mf.rb +29 -29
- data/lib/ruby3mf/thumbnail3mf.rb +21 -21
- data/lib/ruby3mf/version.rb +3 -3
- data/lib/ruby3mf/xml.xsd +286 -286
- data/lib/ruby3mf/xml_val.rb +68 -74
- data/ruby3mf.gemspec +32 -32
- data/suite.011917.out +236 -236
- metadata +3 -9
- data/foo/2D/ffffa2c3-ba74-4bea-a4d0-167a4211134d.model +0 -18747
- data/foo/3D/3dmodel.model +0 -40
- data/foo/3D/_rels/3dmodel.model.rels +0 -4
- data/foo/Thumbnails/ffffa6c3-ba74-4bea-a4d0-167a4211134d.model +0 -0
- data/foo/[Content_Types].xml +0 -7
- data/foo/_rels/.rels +0 -5
@@ -1,73 +1,77 @@
|
|
1
|
-
class ContentTypes
|
2
|
-
|
3
|
-
def initialize(found={}, over={})
|
4
|
-
@found_types=found
|
5
|
-
@found_overrides=over
|
6
|
-
end
|
7
|
-
|
8
|
-
def size
|
9
|
-
@found_types.size + @found_overrides.size
|
10
|
-
end
|
11
|
-
|
12
|
-
def empty?
|
13
|
-
size == 0
|
14
|
-
end
|
15
|
-
|
16
|
-
def get_type(target)
|
17
|
-
target = (target.start_with?('/') ? target : '/' + target).downcase
|
18
|
-
if @found_overrides[target]
|
19
|
-
content_type = @found_overrides[target]
|
20
|
-
else
|
21
|
-
extension = File.extname(target).strip.downcase[1..-1]
|
22
|
-
content_type = @found_types[extension]
|
23
|
-
end
|
24
|
-
content_type
|
25
|
-
end
|
26
|
-
|
27
|
-
def get_types()
|
28
|
-
return @found_types.values + @found_overrides.values
|
29
|
-
end
|
30
|
-
|
31
|
-
private
|
32
|
-
|
33
|
-
def self.parse(zip_entry)
|
34
|
-
found_types = {}
|
35
|
-
found_overrides = {}
|
36
|
-
Log3mf.context "parse" do |l|
|
37
|
-
begin
|
38
|
-
doc = XmlVal.validate_parse(zip_entry)
|
39
|
-
|
40
|
-
l.warning '[Content_Types].xml must contain exactly one root node' unless doc.children.size == 1
|
41
|
-
l.warning '[Content_Types].xml must contain root name Types' unless doc.children.first.name == "Types"
|
42
|
-
|
43
|
-
required_content_types = ['application/vnd.openxmlformats-package.relationships+xml']
|
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
|
-
end
|
1
|
+
class ContentTypes
|
2
|
+
|
3
|
+
def initialize(found={}, over={})
|
4
|
+
@found_types=found
|
5
|
+
@found_overrides=over
|
6
|
+
end
|
7
|
+
|
8
|
+
def size
|
9
|
+
@found_types.size + @found_overrides.size
|
10
|
+
end
|
11
|
+
|
12
|
+
def empty?
|
13
|
+
size == 0
|
14
|
+
end
|
15
|
+
|
16
|
+
def get_type(target)
|
17
|
+
target = (target.start_with?('/') ? target : '/' + target).downcase
|
18
|
+
if @found_overrides[target]
|
19
|
+
content_type = @found_overrides[target]
|
20
|
+
else
|
21
|
+
extension = File.extname(target).strip.downcase[1..-1]
|
22
|
+
content_type = @found_types[extension]
|
23
|
+
end
|
24
|
+
content_type
|
25
|
+
end
|
26
|
+
|
27
|
+
def get_types()
|
28
|
+
return @found_types.values + @found_overrides.values
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def self.parse(zip_entry)
|
34
|
+
found_types = {}
|
35
|
+
found_overrides = {}
|
36
|
+
Log3mf.context "parse" do |l|
|
37
|
+
begin
|
38
|
+
doc = XmlVal.validate_parse(zip_entry)
|
39
|
+
|
40
|
+
l.warning '[Content_Types].xml must contain exactly one root node' unless doc.children.size == 1
|
41
|
+
l.warning '[Content_Types].xml must contain root name Types' unless doc.children.first.name == "Types"
|
42
|
+
|
43
|
+
required_content_types = ['application/vnd.openxmlformats-package.relationships+xml']
|
44
|
+
|
45
|
+
extensions = doc.css(*['Default']).map{|node| node.attributes['Extension']&.value}.flatten
|
46
|
+
l.error :duplicate_content_extension_types unless extensions.uniq.length == extensions.length
|
47
|
+
|
48
|
+
override_extensions = doc.css(*['Override']).map{|node| node.attributes['PartName']&.value}.flatten
|
49
|
+
l.error :duplicate_content_override_types unless override_extensions.uniq.length == override_extensions.length
|
50
|
+
|
51
|
+
found_types = Hash[*doc.css(*['Default']).map{|node| [node.attributes['Extension']&.value&.downcase,node.attributes['ContentType']&.value]}.flatten]
|
52
|
+
found_overrides = Hash[*doc.css(*['Override']).map{|node| [node.attributes['PartName']&.value&.downcase,node.attributes['ContentType']&.value]}.flatten]
|
53
|
+
|
54
|
+
required_content_types.each do |req_type|
|
55
|
+
l.error :invalid_content_type, mt: req_type unless found_types.values.include?(req_type)
|
56
|
+
end
|
57
|
+
|
58
|
+
doc.css(*['Default']).each do |node|
|
59
|
+
extension = node['Extension']&.downcase
|
60
|
+
l.info "Setting type hash #{extension}=#{node['ContentType']}"
|
61
|
+
end
|
62
|
+
|
63
|
+
doc.css(*['Override']).each do |node|
|
64
|
+
l.error :empty_override_part_name if node['PartName']&.downcase&.empty?
|
65
|
+
end
|
66
|
+
|
67
|
+
doc.css('Types').xpath('.//*').select do |node|
|
68
|
+
l.warning "[Content_Types].xml:#{node.line} contains unexpected element #{node.name}", page: 10 unless ['Default', 'Override'].include?(node.name)
|
69
|
+
end
|
70
|
+
|
71
|
+
rescue Nokogiri::XML::SyntaxError => e
|
72
|
+
l.error :content_types_invalid_xml, e: "#{e}"
|
73
|
+
end
|
74
|
+
end
|
75
|
+
return new(found_types, found_overrides)
|
76
|
+
end
|
77
|
+
end
|
data/lib/ruby3mf/document.rb
CHANGED
@@ -1,242 +1,242 @@
|
|
1
|
-
class Document
|
2
|
-
|
3
|
-
attr_accessor :types
|
4
|
-
attr_accessor :relationships
|
5
|
-
attr_accessor :models
|
6
|
-
attr_accessor :thumbnails
|
7
|
-
attr_accessor :textures
|
8
|
-
attr_accessor :objects
|
9
|
-
attr_accessor :parts
|
10
|
-
attr_accessor :zip_filename
|
11
|
-
|
12
|
-
# Relationship schemas
|
13
|
-
MODEL_TYPE = 'http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel'
|
14
|
-
THUMBNAIL_TYPE = 'http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail'
|
15
|
-
TEXTURE_TYPE = 'http://schemas.microsoft.com/3dmanufacturing/2013/01/3dtexture'
|
16
|
-
PRINT_TICKET_TYPE = 'http://schemas.microsoft.com/3dmanufacturing/2013/01/printticket'
|
17
|
-
|
18
|
-
# Image Content Types
|
19
|
-
THUMBNAIL_TYPES = %w[image/jpeg image/png].freeze
|
20
|
-
TEXTURE_TYPES = %w[image/jpeg image/png application/vnd.ms-package.3dmanufacturing-3dmodeltexture].freeze
|
21
|
-
|
22
|
-
# Relationship Type => Class validating relationship type
|
23
|
-
RELATIONSHIP_TYPES = {
|
24
|
-
MODEL_TYPE => {klass: 'Model3mf', collection: :models, valid_types: ['application/vnd.ms-package.3dmanufacturing-3dmodel+xml']},
|
25
|
-
THUMBNAIL_TYPE => {klass: 'Thumbnail3mf', collection: :thumbnails, valid_types: THUMBNAIL_TYPES},
|
26
|
-
TEXTURE_TYPE => {klass: 'Texture3mf', collection: :textures, valid_types: TEXTURE_TYPES},
|
27
|
-
PRINT_TICKET_TYPE => {valid_types: ['application/vnd.ms-printing.printticket+xml']}
|
28
|
-
}
|
29
|
-
|
30
|
-
def initialize(zip_filename)
|
31
|
-
self.models=[]
|
32
|
-
self.thumbnails=[]
|
33
|
-
self.textures=[]
|
34
|
-
self.objects={}
|
35
|
-
self.relationships={}
|
36
|
-
self.types=nil
|
37
|
-
self.parts=[]
|
38
|
-
@zip_filename = zip_filename
|
39
|
-
end
|
40
|
-
|
41
|
-
#verify that each texture part in the 3MF is related to the model through a texture relationship in a rels file
|
42
|
-
def self.validate_texture_parts(document, log)
|
43
|
-
unless document.types.empty?
|
44
|
-
document.parts.select { |part| TEXTURE_TYPES.include?(document.types.get_type(part)) }.each do |tfile|
|
45
|
-
if document.textures.select { |f| f[:target] == tfile }.size == 0
|
46
|
-
if document.thumbnails.select { |t| t[:target] == tfile }.size == 0
|
47
|
-
log.context "part names" do |l|
|
48
|
-
l.warning "#{tfile} appears to be a texture file but no rels file declares any relationship to the model", page: 13
|
49
|
-
end
|
50
|
-
end
|
51
|
-
end
|
52
|
-
end
|
53
|
-
end
|
54
|
-
end
|
55
|
-
|
56
|
-
def self.read(input_file)
|
57
|
-
|
58
|
-
m = new(input_file)
|
59
|
-
begin
|
60
|
-
Log3mf.context 'zip' do |l|
|
61
|
-
begin
|
62
|
-
Zip.warn_invalid_date = false
|
63
|
-
Zip.unicode_names = true
|
64
|
-
|
65
|
-
# check for the general purpose flag set - if so, warn that 3mf may not work on some systems
|
66
|
-
File.open(input_file, "r") do |file|
|
67
|
-
if file.read[6] == "\b"
|
68
|
-
l.warning 'File format: this file may not open on all systems'
|
69
|
-
end
|
70
|
-
end
|
71
|
-
|
72
|
-
Zip::File.open(input_file) do |zip_file|
|
73
|
-
|
74
|
-
l.info 'Zip file is valid'
|
75
|
-
|
76
|
-
# check for valid, absolute URI's for each path name
|
77
|
-
|
78
|
-
zip_file.each do |part|
|
79
|
-
l.context "part names /#{part.name}" do |l|
|
80
|
-
unless part.name.end_with? '[Content_Types].xml'
|
81
|
-
next unless u = parse_uri(l, part.name)
|
82
|
-
|
83
|
-
u.path.split('/').each do |segment|
|
84
|
-
l.error :err_uri_hidden_file if (segment.start_with? '.') && !(segment.end_with? '.rels')
|
85
|
-
end
|
86
|
-
m.parts << '/' + part.name unless part.directory?
|
87
|
-
end
|
88
|
-
end
|
89
|
-
end
|
90
|
-
|
91
|
-
l.context 'content types' do |l|
|
92
|
-
content_type_match = zip_file.glob('\[Content_Types\].xml').first
|
93
|
-
if content_type_match
|
94
|
-
m.types = ContentTypes.parse(content_type_match)
|
95
|
-
else
|
96
|
-
l.fatal_error :missing_content_types
|
97
|
-
end
|
98
|
-
end
|
99
|
-
|
100
|
-
l.context 'relationships' do |l|
|
101
|
-
rel_file = zip_file.glob('_rels/.rels').first
|
102
|
-
l.fatal_error :missing_dot_rels_file unless rel_file
|
103
|
-
|
104
|
-
zip_file.glob('**/*.rels').each do |rel|
|
105
|
-
m.relationships[rel.name] = Relationships.parse(rel)
|
106
|
-
end
|
107
|
-
|
108
|
-
root_rels = m.relationships['_rels/.rels']
|
109
|
-
unless root_rels.nil?
|
110
|
-
start_part_rel = root_rels.select { |rel| rel[:type] == Document::MODEL_TYPE }.first
|
111
|
-
if start_part_rel
|
112
|
-
start_part_target = start_part_rel[:target]
|
113
|
-
start_part_types = m.relationships.flat_map { |k, v| v }.select { |rel| rel[:type] == Document::MODEL_TYPE && rel[:target] == start_part_target }
|
114
|
-
l.error :invalid_startpart_target, :target => start_part_target if start_part_types.size > 1
|
115
|
-
end
|
116
|
-
end
|
117
|
-
|
118
|
-
end
|
119
|
-
|
120
|
-
l.context "print tickets" do |l|
|
121
|
-
print_ticket_types = m.relationships.flat_map { |k, v| v }.select { |rel| rel[:type] == PRINT_TICKET_TYPE }
|
122
|
-
l.error :multiple_print_tickets if print_ticket_types.size > 1
|
123
|
-
end
|
124
|
-
|
125
|
-
l.context "relationship elements" do |l|
|
126
|
-
m.relationships.each do |file_name, rels|
|
127
|
-
rels.each do |rel|
|
128
|
-
l.context rel[:target] do |l|
|
129
|
-
next unless u = parse_uri(l, rel[:target])
|
130
|
-
|
131
|
-
l.error :err_uri_relative_path unless u.to_s.start_with? '/'
|
132
|
-
|
133
|
-
target = rel[:target].gsub(/^\//, "")
|
134
|
-
l.error :err_uri_empty_segment if target.end_with? '/' or target.include? '//'
|
135
|
-
l.error :err_uri_relative_path if target.include? '/../'
|
136
|
-
|
137
|
-
# necessary since it has been observed that rubyzip treats all zip entry names
|
138
|
-
# as ASCII-8BIT regardless if they really contain unicode chars. Without forcing
|
139
|
-
# the encoding we are unable to find the target within the zip if the target contains
|
140
|
-
# unicode chars.
|
141
|
-
zip_target = target
|
142
|
-
zip_target.force_encoding('ASCII-8BIT')
|
143
|
-
relationship_file = zip_file.glob(zip_target).first
|
144
|
-
|
145
|
-
rel_type = rel[:type]
|
146
|
-
if relationship_file
|
147
|
-
relationship_type = RELATIONSHIP_TYPES[rel_type]
|
148
|
-
if relationship_type.nil?
|
149
|
-
l.warning :unsupported_relationship_type, type: rel[:type], target: rel[:target]
|
150
|
-
else
|
151
|
-
# check that relationships are valid; extensions and relationship types must jive
|
152
|
-
content_type = m.types.get_type(target)
|
153
|
-
expected_content_type = relationship_type[:valid_types]
|
154
|
-
|
155
|
-
if (expected_content_type)
|
156
|
-
l.error :missing_content_type, part: target unless content_type
|
157
|
-
l.error :resource_contentype_invalid, bt: content_type, rt: rel[:target] unless (content_type.nil? || expected_content_type.include?(content_type))
|
158
|
-
else
|
159
|
-
l.info "found unrecognized relationship type: #{rel_type}"
|
160
|
-
end
|
161
|
-
|
162
|
-
unless relationship_type[:klass].nil?
|
163
|
-
m.send(relationship_type[:collection]) << {
|
164
|
-
rel_id: rel[:id],
|
165
|
-
target: rel[:target],
|
166
|
-
object: Object.const_get(relationship_type[:klass]).parse(m, relationship_file)
|
167
|
-
}
|
168
|
-
end
|
169
|
-
end
|
170
|
-
else
|
171
|
-
l.error :rel_file_not_found, mf: "#{rel[:target]}"
|
172
|
-
end
|
173
|
-
end
|
174
|
-
end
|
175
|
-
end
|
176
|
-
end
|
177
|
-
|
178
|
-
validate_texture_parts(m, l)
|
179
|
-
end
|
180
|
-
|
181
|
-
return m
|
182
|
-
rescue Zip::Error
|
183
|
-
l.fatal_error :not_a_zip
|
184
|
-
end
|
185
|
-
end
|
186
|
-
rescue Log3mf::FatalError
|
187
|
-
#puts "HALTING PROCESSING DUE TO FATAL ERROR"
|
188
|
-
return nil
|
189
|
-
end
|
190
|
-
end
|
191
|
-
|
192
|
-
def write(output_file = nil)
|
193
|
-
output_file = zip_filename if output_file.nil?
|
194
|
-
|
195
|
-
Zip::File.open(zip_filename) do |input_zip_file|
|
196
|
-
|
197
|
-
buffer = Zip::OutputStream.write_buffer do |out|
|
198
|
-
input_zip_file.entries.each do |e|
|
199
|
-
if e.directory?
|
200
|
-
out.copy_raw_entry(e)
|
201
|
-
else
|
202
|
-
out.put_next_entry(e.name)
|
203
|
-
if objects[e.name]
|
204
|
-
out.write objects[e.name]
|
205
|
-
else
|
206
|
-
out.write e.get_input_stream.read
|
207
|
-
end
|
208
|
-
end
|
209
|
-
end
|
210
|
-
end
|
211
|
-
|
212
|
-
File.open(output_file, 'wb') { |f| f.write(buffer.string) }
|
213
|
-
|
214
|
-
end
|
215
|
-
|
216
|
-
end
|
217
|
-
|
218
|
-
def contents_for(path)
|
219
|
-
Zip::File.open(zip_filename) do |zip_file|
|
220
|
-
zip_file.glob(path).first.get_input_stream.read
|
221
|
-
end
|
222
|
-
end
|
223
|
-
|
224
|
-
private
|
225
|
-
|
226
|
-
def self.parse_uri(logger, uri)
|
227
|
-
begin
|
228
|
-
reserved_chars = /[\[\]]/
|
229
|
-
|
230
|
-
if uri =~ reserved_chars
|
231
|
-
logger.error :err_uri_bad
|
232
|
-
return nil
|
233
|
-
end
|
234
|
-
|
235
|
-
u = Addressable::URI.parse uri
|
236
|
-
rescue ArgumentError, Addressable::URI::InvalidURIError
|
237
|
-
logger.error :err_uri_bad
|
238
|
-
end
|
239
|
-
u
|
240
|
-
end
|
241
|
-
|
242
|
-
end
|
1
|
+
class Document
|
2
|
+
|
3
|
+
attr_accessor :types
|
4
|
+
attr_accessor :relationships
|
5
|
+
attr_accessor :models
|
6
|
+
attr_accessor :thumbnails
|
7
|
+
attr_accessor :textures
|
8
|
+
attr_accessor :objects
|
9
|
+
attr_accessor :parts
|
10
|
+
attr_accessor :zip_filename
|
11
|
+
|
12
|
+
# Relationship schemas
|
13
|
+
MODEL_TYPE = 'http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel'
|
14
|
+
THUMBNAIL_TYPE = 'http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail'
|
15
|
+
TEXTURE_TYPE = 'http://schemas.microsoft.com/3dmanufacturing/2013/01/3dtexture'
|
16
|
+
PRINT_TICKET_TYPE = 'http://schemas.microsoft.com/3dmanufacturing/2013/01/printticket'
|
17
|
+
|
18
|
+
# Image Content Types
|
19
|
+
THUMBNAIL_TYPES = %w[image/jpeg image/png].freeze
|
20
|
+
TEXTURE_TYPES = %w[image/jpeg image/png application/vnd.ms-package.3dmanufacturing-3dmodeltexture].freeze
|
21
|
+
|
22
|
+
# Relationship Type => Class validating relationship type
|
23
|
+
RELATIONSHIP_TYPES = {
|
24
|
+
MODEL_TYPE => {klass: 'Model3mf', collection: :models, valid_types: ['application/vnd.ms-package.3dmanufacturing-3dmodel+xml']},
|
25
|
+
THUMBNAIL_TYPE => {klass: 'Thumbnail3mf', collection: :thumbnails, valid_types: THUMBNAIL_TYPES},
|
26
|
+
TEXTURE_TYPE => {klass: 'Texture3mf', collection: :textures, valid_types: TEXTURE_TYPES},
|
27
|
+
PRINT_TICKET_TYPE => {valid_types: ['application/vnd.ms-printing.printticket+xml']}
|
28
|
+
}
|
29
|
+
|
30
|
+
def initialize(zip_filename)
|
31
|
+
self.models=[]
|
32
|
+
self.thumbnails=[]
|
33
|
+
self.textures=[]
|
34
|
+
self.objects={}
|
35
|
+
self.relationships={}
|
36
|
+
self.types=nil
|
37
|
+
self.parts=[]
|
38
|
+
@zip_filename = zip_filename
|
39
|
+
end
|
40
|
+
|
41
|
+
#verify that each texture part in the 3MF is related to the model through a texture relationship in a rels file
|
42
|
+
def self.validate_texture_parts(document, log)
|
43
|
+
unless document.types.empty?
|
44
|
+
document.parts.select { |part| TEXTURE_TYPES.include?(document.types.get_type(part)) }.each do |tfile|
|
45
|
+
if document.textures.select { |f| f[:target] == tfile }.size == 0
|
46
|
+
if document.thumbnails.select { |t| t[:target] == tfile }.size == 0
|
47
|
+
log.context "part names" do |l|
|
48
|
+
l.warning "#{tfile} appears to be a texture file but no rels file declares any relationship to the model", page: 13
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.read(input_file)
|
57
|
+
|
58
|
+
m = new(input_file)
|
59
|
+
begin
|
60
|
+
Log3mf.context 'zip' do |l|
|
61
|
+
begin
|
62
|
+
Zip.warn_invalid_date = false
|
63
|
+
Zip.unicode_names = true
|
64
|
+
|
65
|
+
# check for the general purpose flag set - if so, warn that 3mf may not work on some systems
|
66
|
+
File.open(input_file, "r") do |file|
|
67
|
+
if file.read[6] == "\b"
|
68
|
+
l.warning 'File format: this file may not open on all systems'
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
Zip::File.open(input_file) do |zip_file|
|
73
|
+
|
74
|
+
l.info 'Zip file is valid'
|
75
|
+
|
76
|
+
# check for valid, absolute URI's for each path name
|
77
|
+
|
78
|
+
zip_file.each do |part|
|
79
|
+
l.context "part names /#{part.name}" do |l|
|
80
|
+
unless part.name.end_with? '[Content_Types].xml'
|
81
|
+
next unless u = parse_uri(l, part.name)
|
82
|
+
|
83
|
+
u.path.split('/').each do |segment|
|
84
|
+
l.error :err_uri_hidden_file if (segment.start_with? '.') && !(segment.end_with? '.rels')
|
85
|
+
end
|
86
|
+
m.parts << '/' + part.name unless part.directory?
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
l.context 'content types' do |l|
|
92
|
+
content_type_match = zip_file.glob('\[Content_Types\].xml').first
|
93
|
+
if content_type_match
|
94
|
+
m.types = ContentTypes.parse(content_type_match)
|
95
|
+
else
|
96
|
+
l.fatal_error :missing_content_types
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
l.context 'relationships' do |l|
|
101
|
+
rel_file = zip_file.glob('_rels/.rels').first
|
102
|
+
l.fatal_error :missing_dot_rels_file unless rel_file
|
103
|
+
|
104
|
+
zip_file.glob('**/*.rels').each do |rel|
|
105
|
+
m.relationships[rel.name] = Relationships.parse(rel)
|
106
|
+
end
|
107
|
+
|
108
|
+
root_rels = m.relationships['_rels/.rels']
|
109
|
+
unless root_rels.nil?
|
110
|
+
start_part_rel = root_rels.select { |rel| rel[:type] == Document::MODEL_TYPE }.first
|
111
|
+
if start_part_rel
|
112
|
+
start_part_target = start_part_rel[:target]
|
113
|
+
start_part_types = m.relationships.flat_map { |k, v| v }.select { |rel| rel[:type] == Document::MODEL_TYPE && rel[:target] == start_part_target }
|
114
|
+
l.error :invalid_startpart_target, :target => start_part_target if start_part_types.size > 1
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
end
|
119
|
+
|
120
|
+
l.context "print tickets" do |l|
|
121
|
+
print_ticket_types = m.relationships.flat_map { |k, v| v }.select { |rel| rel[:type] == PRINT_TICKET_TYPE }
|
122
|
+
l.error :multiple_print_tickets if print_ticket_types.size > 1
|
123
|
+
end
|
124
|
+
|
125
|
+
l.context "relationship elements" do |l|
|
126
|
+
m.relationships.each do |file_name, rels|
|
127
|
+
rels.each do |rel|
|
128
|
+
l.context rel[:target] do |l|
|
129
|
+
next unless u = parse_uri(l, rel[:target])
|
130
|
+
|
131
|
+
l.error :err_uri_relative_path unless u.to_s.start_with? '/'
|
132
|
+
|
133
|
+
target = rel[:target].gsub(/^\//, "")
|
134
|
+
l.error :err_uri_empty_segment if target.end_with? '/' or target.include? '//'
|
135
|
+
l.error :err_uri_relative_path if target.include? '/../'
|
136
|
+
|
137
|
+
# necessary since it has been observed that rubyzip treats all zip entry names
|
138
|
+
# as ASCII-8BIT regardless if they really contain unicode chars. Without forcing
|
139
|
+
# the encoding we are unable to find the target within the zip if the target contains
|
140
|
+
# unicode chars.
|
141
|
+
zip_target = target
|
142
|
+
zip_target.force_encoding('ASCII-8BIT')
|
143
|
+
relationship_file = zip_file.glob(zip_target).first
|
144
|
+
|
145
|
+
rel_type = rel[:type]
|
146
|
+
if relationship_file
|
147
|
+
relationship_type = RELATIONSHIP_TYPES[rel_type]
|
148
|
+
if relationship_type.nil?
|
149
|
+
l.warning :unsupported_relationship_type, type: rel[:type], target: rel[:target]
|
150
|
+
else
|
151
|
+
# check that relationships are valid; extensions and relationship types must jive
|
152
|
+
content_type = m.types.get_type(target)
|
153
|
+
expected_content_type = relationship_type[:valid_types]
|
154
|
+
|
155
|
+
if (expected_content_type)
|
156
|
+
l.error :missing_content_type, part: target unless content_type
|
157
|
+
l.error :resource_contentype_invalid, bt: content_type, rt: rel[:target] unless (content_type.nil? || expected_content_type.include?(content_type))
|
158
|
+
else
|
159
|
+
l.info "found unrecognized relationship type: #{rel_type}"
|
160
|
+
end
|
161
|
+
|
162
|
+
unless relationship_type[:klass].nil?
|
163
|
+
m.send(relationship_type[:collection]) << {
|
164
|
+
rel_id: rel[:id],
|
165
|
+
target: rel[:target],
|
166
|
+
object: Object.const_get(relationship_type[:klass]).parse(m, relationship_file)
|
167
|
+
}
|
168
|
+
end
|
169
|
+
end
|
170
|
+
else
|
171
|
+
l.error :rel_file_not_found, mf: "#{rel[:target]}"
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
validate_texture_parts(m, l)
|
179
|
+
end
|
180
|
+
|
181
|
+
return m
|
182
|
+
rescue Zip::Error
|
183
|
+
l.fatal_error :not_a_zip
|
184
|
+
end
|
185
|
+
end
|
186
|
+
rescue Log3mf::FatalError
|
187
|
+
#puts "HALTING PROCESSING DUE TO FATAL ERROR"
|
188
|
+
return nil
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
def write(output_file = nil)
|
193
|
+
output_file = zip_filename if output_file.nil?
|
194
|
+
|
195
|
+
Zip::File.open(zip_filename) do |input_zip_file|
|
196
|
+
|
197
|
+
buffer = Zip::OutputStream.write_buffer do |out|
|
198
|
+
input_zip_file.entries.each do |e|
|
199
|
+
if e.directory?
|
200
|
+
out.copy_raw_entry(e)
|
201
|
+
else
|
202
|
+
out.put_next_entry(e.name)
|
203
|
+
if objects[e.name]
|
204
|
+
out.write objects[e.name]
|
205
|
+
else
|
206
|
+
out.write e.get_input_stream.read
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
File.open(output_file, 'wb') { |f| f.write(buffer.string) }
|
213
|
+
|
214
|
+
end
|
215
|
+
|
216
|
+
end
|
217
|
+
|
218
|
+
def contents_for(path)
|
219
|
+
Zip::File.open(zip_filename) do |zip_file|
|
220
|
+
zip_file.glob(path).first.get_input_stream.read
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
private
|
225
|
+
|
226
|
+
def self.parse_uri(logger, uri)
|
227
|
+
begin
|
228
|
+
reserved_chars = /[\[\]]/
|
229
|
+
|
230
|
+
if uri =~ reserved_chars
|
231
|
+
logger.error :err_uri_bad
|
232
|
+
return nil
|
233
|
+
end
|
234
|
+
|
235
|
+
u = Addressable::URI.parse uri
|
236
|
+
rescue ArgumentError, Addressable::URI::InvalidURIError
|
237
|
+
logger.error :err_uri_bad
|
238
|
+
end
|
239
|
+
u
|
240
|
+
end
|
241
|
+
|
242
|
+
end
|