metalink4-ruby 1.0.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: d43e73fcf1bfd375a6b933529250b3988afe90ef31c85d5f26e6758cf6a0a589
4
+ data.tar.gz: 165dc4c7395d9d7a52b25602c6f09aa93671d03075e982a30855e7474d5fbaa8
5
+ SHA512:
6
+ metadata.gz: 16d8851908730101751a1925cd3577beb4a458dade1f205c393d97ec9d651a4ef728b86981aa27974b8087fd6a432e4c141076368747ffd3bf5f2ea2fcbfe0fe
7
+ data.tar.gz: 801830a5d7c08fa239f4a8a5f7fd5c20e1488c584d0da1a526bc396968d30a2fabe2fd2bfb021ada7449e8b4eda6a9cda0d239ab8b57a6e9e614f2c3519d7f0c
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2021 Sudrien
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,82 @@
1
+ # metalink4-ruby
2
+
3
+ Class to format Metalink 4 / rfc5854 / .meta4 XML
4
+
5
+
6
+ All rfc5854 metadata is supported, as well as Chunk checksumming, for
7
+ detecting errors early on bad internet connections - see piece_size and
8
+ piece_count options.
9
+
10
+ All internally generated checksums are sha-256. Other checksum types
11
+ must be calculates externally and passed in.
12
+
13
+
14
+ No code taken from timsjoberg/metalink-ruby.
15
+
16
+
17
+ ## Installation
18
+
19
+ Requirements:
20
+
21
+ - Ruby >= 1.9.3
22
+
23
+ ``` sh
24
+ $ gem install metalink4-ruby
25
+ ```
26
+
27
+
28
+ ## Generation Examples
29
+
30
+ Please see https://github.com/Sudrien/metalink4-ruby/blob/main/test/test.rb
31
+
32
+
33
+ ## Download Examples
34
+
35
+ metalink4-ruby 1.0.0 allows for the parsing of external meta4 files, but
36
+ does not support downloading and verifying checksums, as metalink supports
37
+ the listing of many protocols.
38
+
39
+ A presumed outdated list of clients is available at http://www.metalinker.org/implementation.html
40
+
41
+ Here is a partial list for convienece of easily scriptable programs for
42
+ convienence.
43
+
44
+ ### curl
45
+ As of 7.78.0, curl removed Metalink support, rather than support a 'failure'
46
+ over a 'warning' when checksums did not match.
47
+
48
+ See: https://curl.se/docs/CVE-2021-22922.html
49
+
50
+
51
+
52
+
53
+ ### aria2c example (recommended)
54
+ (Note: It is possible to compile binaries without metalink support)
55
+
56
+ Show file list
57
+ > aria2c test/1MB.meta4 -S
58
+
59
+ Download single file on that list
60
+ > aria2c test/1MB.meta4 --select-file=1
61
+
62
+ Check integrity of any files on disk instead of overwriting
63
+ > aria2c test/1MB.meta4 --check-integrity=true
64
+
65
+ More:
66
+ * https://aria2.github.io/manual/en/html/aria2c.html#bittorrent-metalink-options
67
+ * https://aria2.github.io/manual/en/html/aria2c.html#metalink-specific-options
68
+
69
+
70
+
71
+
72
+ ### wget example (partial support)
73
+ (Note: It is possible to compile binaries without metalink support)
74
+
75
+ Download all files? 1MB.meta4.#1 a result, not obvious how to get correct filename
76
+ > wget --input-metalink=test/1MB.meta4
77
+
78
+ Download single file on that list, supposedly
79
+ > wget --input-metalink=test/1MB.meta4 --metalink-index=1
80
+
81
+ More:
82
+ * https://www.gnu.org/software/wget/manual/html_node/Logging-and-Input-File-Options.html
data/lib/metalink4.rb ADDED
@@ -0,0 +1,607 @@
1
+
2
+ require 'builder'
3
+ require 'pathname'
4
+ require 'digest'
5
+ require 'mime/types'
6
+ require 'time'
7
+ require 'nokogiri'
8
+ require 'uri'
9
+
10
+ ##
11
+ # Because hash can't just be a string. We need to specify what type.
12
+
13
+ class Metalink4FileHash
14
+ attr_accessor :hash_value,
15
+ :hash_type,
16
+ :piece
17
+
18
+ def initialize(opts = {})
19
+ opts = opts.inject({}){ |r, (k,v)| r[k.to_sym] = v; r }
20
+
21
+ self.hash_value = opts.fetch(:hash_value, nil)
22
+ self.hash_type = opts.fetch(:hash_type, nil)
23
+ self.piece = opts.fetch(:piece, nil)
24
+ end
25
+
26
+ ##
27
+ # Hash must be in hexidecimal, lowercase
28
+ #
29
+ def hash_value=(v)
30
+ raise ("Improper Format '%s'" % v) if v && v !~ /[0-9a-f]+/
31
+ @hash_value = v
32
+ end
33
+
34
+ ##
35
+ # sha-256
36
+ #
37
+ def hash_type=(v)
38
+ @hash_type = v
39
+ end
40
+
41
+ ##
42
+ # nil if of whole file.
43
+ #
44
+ def piece=(v)
45
+ @piece = v
46
+ end
47
+
48
+ end
49
+
50
+
51
+
52
+ ##
53
+ # Describes the download urls of files listed in the Metalink 4 file
54
+ class Metalink4FileUrl
55
+ attr_accessor :url,
56
+ :priority,
57
+ :location
58
+ ##
59
+ # Options: url, location, priority.
60
+ # if only a string it profided it must be the URL.
61
+ def initialize(opts = {})
62
+
63
+ self.priority = 1
64
+ case opts
65
+ when String
66
+ self.url = opts
67
+ when Hash
68
+ opts = opts.inject({}){ |r, (k,v)| r[k.to_sym] = v; r }
69
+ self.url = opts[:url]
70
+ self.location = opts[:location]
71
+ self.priority = opts[:priority]
72
+ else
73
+ raise "URL format %s" % opts.inspect
74
+ end
75
+ end
76
+
77
+ ##
78
+ # URI of remote file, HTTPS, HTTP, FTP, Bittorrent, etc.
79
+ # One, Required.
80
+ def url=(v)
81
+ @url = v ? URI.parse(v) : nil
82
+ end
83
+
84
+ ##
85
+ # ISO3166-1 2 character country code
86
+ # Client may ignore this field if a country code is not explicitly specified.
87
+ # One, optional.
88
+ def location=(v)
89
+ raise "Improper format" if v && v.to_s !~ /[a-z]{2}/
90
+ @location = v
91
+ end
92
+
93
+ ##
94
+ # Priority this url is to be considered in. 1 is top level, 999999 is absolulte last. Duplicates allowed.
95
+ # Defaults to 1 if not specified.
96
+ def priority=(v)
97
+ @priority = [[v.to_i, 1].max, 999999].min
98
+ end
99
+
100
+ ##
101
+ # Fragement call for builder.
102
+ # For internal use.
103
+ def render(builder_metalink_file, local_path)
104
+ begin
105
+ if MIME::Types.type_for(local_path.to_s).first == MIME::Types.type_for( self.url.path ).first
106
+ builder_metalink_file.url(
107
+ self.url.to_s, {
108
+ location: self.location,
109
+ priority: self.priority || 1,
110
+ }.delete_if{ |k, v| v.nil? }
111
+ )
112
+ else
113
+ builder_metalink_file.metaurl(
114
+ self.url.to_s, {
115
+ priority: self.priority || 1,
116
+ mediatype: self.url.path =~ /\.torrent/ ? "torrent" : MIME::Types.type_for( self.url.path ).first.to_s
117
+ }.delete_if{ |k, v| v.nil? }
118
+ )
119
+ end
120
+ rescue
121
+ puts [local_path, self.url]
122
+ throw [local_path, self.url]
123
+ end
124
+ end
125
+ end
126
+
127
+ ##
128
+ #Describes the files listed in the Metalink 4 file
129
+ class Metalink4File
130
+
131
+ attr_accessor :local_path,
132
+ :copyright,
133
+ :description,
134
+ :identity,
135
+ :language,
136
+ :logo,
137
+ :os,
138
+ :urls,
139
+ :publisher_name,
140
+ :publisher_url,
141
+ :signature,
142
+ :version,
143
+ :piece_size,
144
+ :piece_count,
145
+ :hashes,
146
+ :size
147
+
148
+ ##
149
+ # Options: local_path, copyright, description, identity, language, logo, os, urls, publisher_name, publisher_url, signature, version, piece_size, piece_count
150
+ def initialize(opts = {})
151
+ opts = opts.inject({}){ |r, (k,v)| r[k.to_sym] = v; r }
152
+
153
+ self.language = []
154
+ self.os = []
155
+
156
+
157
+ self.local_path = opts.fetch(:local_path, nil)
158
+ self.copyright = opts.fetch(:copyright, nil)
159
+ self.description = opts.fetch(:description, nil)
160
+ self.identity = opts.fetch(:identity, nil)
161
+ case opts.fetch(:language, nil)
162
+ when Array
163
+ self.language = opts.fetch(:language, nil)
164
+ when String
165
+ self.language = [opts.fetch(:language, nil)]
166
+ end
167
+
168
+ self.logo = opts.fetch(:logo, nil)
169
+ case opts.fetch(:os, nil)
170
+ when Array
171
+ self.os = opts.fetch(:os, nil)
172
+ when String
173
+ self.os = [opts.fetch(:os, nil)]
174
+ end
175
+
176
+ self.urls = []
177
+ opts.fetch(:urls, []).each do |url|
178
+ self.urls << Metalink4FileUrl.new(url)
179
+ end
180
+
181
+ self.publisher_name = opts.fetch(:publisher_name, nil)
182
+ self.publisher_url = opts.fetch(:publisher_url, nil)
183
+ self.signature = opts.fetch(:signature, nil)
184
+ self.version = opts.fetch(:version, nil)
185
+
186
+ self.piece_size = opts.fetch(:piece_size, nil)
187
+ self.piece_count = opts.fetch(:piece_count, nil)
188
+
189
+ self.hashes = []
190
+ end
191
+
192
+ ##
193
+ # Path of local instance of file, relative to current working directory.
194
+ # relative path may be reproduced on the client side. MUST NOT include '.' or '..'
195
+ # Must not include an absolute path ( begin with / or drive letter )
196
+ #
197
+ # use File.chdir if you must to achive this
198
+ # Required
199
+ def local_path=(v)
200
+ return unless v
201
+ @local_path = Pathname.new(v)
202
+ raise "No absolute paths" if @local_path.absolute?
203
+ raise "No dots" if @local_path.to_s =~ /\.\/|\.\\/
204
+ @local_path
205
+ end
206
+
207
+
208
+ ##
209
+ # The copyright of the file, human readble. URL should be ok, Or a full text.
210
+ # Lack of this field does not assert ANY paticular state of copyright.
211
+ # One, optional.
212
+ def copyright=(v)
213
+ @copyright = v
214
+ end
215
+
216
+
217
+ ##
218
+ # if "Firefox 3.5" the description would be "A Web Browser"
219
+ # Human readable.
220
+ # One, optional, but reccomended.
221
+ def description=(v)
222
+ @description = v
223
+ end
224
+
225
+ ##
226
+ # if "Firefox 3.5" the identity would be "Firefox"
227
+ # Human readable.
228
+ # One, optional.
229
+ def identity=(v)
230
+ @identity = v
231
+ end
232
+
233
+ ##
234
+ # The language supported by the file.
235
+ # In rfc5646 format
236
+ # See: https://datatracker.ietf.org/doc/html/rfc5646
237
+ # One or more, optional.
238
+ def language=(v)
239
+ @language = v.is_a?(String) ? [v] : v
240
+ end
241
+
242
+ ##
243
+ # Logo is the URL of an image file associated with this file. This can be an icon or avatar.
244
+ # It should be square, and support low resolutions.
245
+ # One, optional.
246
+ def logo=(v)
247
+ @logo = v ? URI.parse(v) : nil
248
+ end
249
+
250
+ ##
251
+ # Operating System this download supports. One or more, optional.
252
+ # See IANA registry named "Operating System Names"
253
+ # at https://www.iana.org/assignments/operating-system-names/operating-system-names.xhtml
254
+ def os=(v)
255
+ @os = v.is_a?(String) ? [v] : v
256
+ end
257
+
258
+ ##
259
+ # The creator of this meta4 file, which may be differnt than the files refrenced within.
260
+ # One, Optional.
261
+ def publisher_name=(v)
262
+ @publisher_name = v
263
+ end
264
+
265
+ ##
266
+ # THE URL of the publisher, a descriptive page, NOT the source of this .meta4 file.
267
+ # One, Optional.
268
+ def publisher_url=(v)
269
+ @publisher_url = v ? URI.parse(v) : nil
270
+ end
271
+
272
+
273
+ ##
274
+ # OpenPGP Or somthing. The signature should match the referenced file.
275
+ # One, Optional.
276
+ def signature=(v)
277
+ @signature = v
278
+ end
279
+
280
+ ##
281
+ # if "Firefox 3.5" the version would be "3.5"
282
+ # Human readable.
283
+ # One, optional.
284
+ def version=(v)
285
+ @version = v
286
+ end
287
+
288
+ ##
289
+ # Overwrite any present hashes. If no path is provided, checksum will be attempted of local_file.
290
+ # This might require a Dir.chdir.
291
+ #
292
+ # There is no protection of what this path can call - please sanitize any input to make sure files
293
+ # outside your project can't be checksummed.
294
+ #
295
+ # options: debug - show full path on error dump, piece_size - bytes, integer, OR piece_count - integer
296
+ #
297
+ # These hashes will be SHA256
298
+ def checksum!(path = nil, opts = {})
299
+ opts = opts.inject({}){ |r, (k,v)| r[k.to_sym] = v; r }
300
+
301
+ raise "Path uses differnt extension" if path && File.extname(path) != File.extname(self.local_path)
302
+
303
+
304
+ if opts[:debug]
305
+ raise ("File '%s' does not exist" % File.expand_path( path ) ) if path && !File.exist?( File.expand_path( path ) )
306
+ else
307
+ raise ("File '%s' does not exist" % path ) if path && !File.exist?( File.expand_path( path ) )
308
+ end
309
+
310
+ raise "Can not specify both piece_size and piece_count" if opts[:piece_size] && opts[:piece_count]
311
+ raise "piece_size must be an Integer" if opts[:piece_size] && !opts[:piece_size].is_a?(Integer)
312
+ raise "piece_count must be an Integer" if opts[:piece_count] && !opts[:piece_count].is_a?(Integer)
313
+
314
+ path ||= self.local_path
315
+
316
+ #get filesize
317
+ self.size = File.size( File.expand_path( path ) )
318
+
319
+ # Overrides piece_count if set
320
+ # minimum size of 1KB for a piece
321
+ if opts[:piece_size]
322
+ @piece_count = nil
323
+ @piece_size = [opts[:piece_size], 1024].max
324
+ end
325
+
326
+
327
+ # Overrides piece_size if set
328
+ # This will be a multiple of 1024, and the last file will be slightly smaller
329
+ # this means 1KB is the minimum size for a piece
330
+ if opts[:piece_count]
331
+ @piece_count = opts[:piece_count]
332
+ @piece_size = ((@size / @piece_count) / 1024.0).ceil * 1024
333
+ end
334
+
335
+ self.hashes = []
336
+ self.hashes << Metalink4FileHash.new( hash_value: Digest::SHA256.file( File.expand_path( path ) ).hexdigest, hash_type: "sha-256" )
337
+ if self.piece_size
338
+ i = 0
339
+ (0...@size).step(self.piece_size).each do |offset|
340
+ self.hashes << Metalink4FileHash.new( hash_value: Digest::SHA256.hexdigest(File.read(File.expand_path( path ), self.piece_size, offset)), hash_type: "sha-256", piece: i )
341
+ i += 1
342
+ end
343
+ end
344
+ end
345
+
346
+
347
+ ##
348
+ # Fragement call for builder.
349
+ # For internal use.
350
+ def render(builder_metalink, metalink4)
351
+
352
+ raise "Local path required" if !@local_path
353
+ #raise ("Local path '%s' does not match any files" % @local_path) if File.exist?(@local_path) || !self.hashes.empty?
354
+
355
+ #build up hashes if none were imported
356
+ if self.hashes.empty? && File.exist?(@local_path)
357
+ self.checksum!
358
+ end
359
+
360
+ metalink4.file( name: self.local_path ) do |metalink_file|
361
+ metalink_file.copyright( ("\n%s\n" % self.copyright) ) if self.copyright
362
+ metalink_file.description( self.description ) if self.description
363
+ metalink_file.identity( self.identity ) if self.identity
364
+
365
+ self.hashes.select{ |x| x.piece.nil? }.each do |hash|
366
+ metalink_file.hash(hash.hash_value, type: hash.hash_type)
367
+ end
368
+
369
+ #multiple languages possible
370
+ self.language.each do |language|
371
+ metalink_file.language( language )
372
+ end if self.language
373
+
374
+ metalink_file.logo( self.logo ) if self.logo
375
+
376
+ #multiple operating systems possible
377
+ self.os.each do |os|
378
+ metalink_file.os( os )
379
+ end if self.os
380
+
381
+ self.urls.each do |file_url|
382
+ file_url.render(
383
+ metalink_file,
384
+ self.local_path
385
+ )
386
+ end if self.urls
387
+
388
+ metalink_file.pieces(length: self.piece_size, type: self.hashes.select{ |x| x.piece }.first.hash_type) do |pieces|
389
+ self.hashes.select{ |x| x.piece }.sort_by(&:piece).each do |hash|
390
+ metalink_file.hash(hash.hash_value)
391
+ end
392
+ end if self.hashes.any?{ |x| x.piece }
393
+
394
+ metalink_file.publisher( name: self.publisher_name, url: self.publisher_url.to_s) if self.publisher_name || self.publisher_url
395
+
396
+ #what other type is it going to be
397
+ metalink_file.signature( ("\n%s\n" % self.signature), mediatype: "application/pgp-signature" ) if self.signature
398
+
399
+ metalink_file.size( self.size ) #SHOULD, file bytesize
400
+ metalink_file.version( self.version ) if self.version
401
+ end
402
+ end
403
+
404
+
405
+
406
+ ##
407
+ # One of two options to enable piece hashes
408
+ # Overrides piece_count if set
409
+ # minimum size of 1KB for a piece
410
+ def piece_size=(v)
411
+ return unless v
412
+ @piece_count = nil
413
+ @piece_size = [v, 1024].max
414
+ end
415
+
416
+ private
417
+
418
+ ##
419
+ # private becaue checksum! is a preferred call
420
+ # One of two options to enable piece hashes
421
+ # Overrides piece_size if set
422
+ # This will be a multiple of 1024, and the last file will be slightly smaller
423
+ # this means 1KB is the minimum size for a piece
424
+ def piece_count=(v)
425
+ return if v.nil?
426
+ @piece_count = v
427
+ @piece_size = ((@local_path.size / v) / 1024.0).ceil * 1024
428
+ end
429
+
430
+
431
+ end
432
+
433
+ ##
434
+ # Describes the base Metalink 4 file as specified by rfc5854
435
+ # If served, it should have a header of application/metalink4+xml
436
+ # https://en.wikipedia.org/wiki/Metalink
437
+ class Metalink4
438
+
439
+ attr_accessor :files,
440
+ :published,
441
+ :updated,
442
+ :origin,
443
+ :origin_dynamic,
444
+ :xml
445
+
446
+ ##
447
+ # Options: files, published, updated, origin, origin_dynamic
448
+ def initialize(opts = {})
449
+ opts = opts.inject({}){ |r, (k,v)| r[k.to_sym] = v; r }
450
+
451
+ self.files = []
452
+
453
+ opts.fetch(:files, []).each do |file|
454
+ self.files << Metalink4File.new(file)
455
+ end
456
+
457
+ self.published = opts.fetch(:published, nil)
458
+ self.updated = opts.fetch(:updated, nil)
459
+ self.origin = opts.fetch(:origin, nil)
460
+ self.origin_dynamic = opts.fetch(:origin_dynamic, false)
461
+
462
+ self.xml = Builder::XmlMarkup.new(indent: 2)
463
+ end
464
+
465
+ ##
466
+ # original publish date. Equivelent of ActiveRecord's created_at
467
+ # One, Optional.
468
+ def published=(v)
469
+ case v
470
+ when Time, nil
471
+ @published = v
472
+ when String
473
+ @published = Time.parse(v)
474
+ else
475
+ raise "Not a Time"
476
+ end
477
+ @published
478
+ end
479
+
480
+ ##
481
+ # last publish date. Equivelent of ActiveRecord's updated_at
482
+ # One, Optional.
483
+ def updated=(v)
484
+ case v
485
+ when Time, nil
486
+ @updated = v
487
+ when String
488
+ @updated = Time.parse(v)
489
+ else
490
+ raise "Not a Time"
491
+ end
492
+ @updated
493
+ end
494
+
495
+ ##
496
+ # url this meta4 file was made available at. Updates are potentially at the same url.
497
+ # One, Optional, but should.
498
+ def origin=(v)
499
+ @origin = v ? URI.parse(v) : nil
500
+ end
501
+
502
+ ##
503
+ # if the meta4 file may have an update available
504
+ # Defaults to false.
505
+ def origin_dynamic=(v)
506
+ @origin = !!v
507
+ end
508
+
509
+
510
+ ##
511
+ # Render to XML, returns string.
512
+ #
513
+ # Checksums are calculated at this point. ONLY sha-256 is calculated.
514
+ def render
515
+
516
+ raise "files not specified" if self.files.empty?
517
+
518
+ self.xml.instruct! :xml, version: "1.0", encoding: "UTF-8"
519
+ self.xml.metalink( xmlns: "urn:ietf:params:xml:ns:metalink" ) do |metalink|
520
+
521
+ metalink.generator("Sudrien/metalink-ruby 0.2.0") #may
522
+ metalink.origin( self.origin, dynamic: self.origin_dynamic ) if self.origin
523
+
524
+ metalink.published( self.published.strftime('%FT%T%:z') ) if self.published
525
+ metalink.updated( self.updated.strftime('%FT%T%:z') ) if self.updated
526
+
527
+ self.files.each do |file|
528
+ file.render(self, metalink)
529
+ end
530
+ end
531
+ self.xml.target!
532
+ end
533
+
534
+ ##
535
+ # Read a .meta4 to internal model. Takes either a file path or XML sting.
536
+ #
537
+ # Will import any provided hashes, so nothing needs to be calculated.
538
+ def self.read(potential_file_path)
539
+
540
+ begin
541
+ if File.exist?(potential_file_path)
542
+ doc = File.open(potential_file_path) { |f| Nokogiri::XML(f) }
543
+ elsif potential_file_path.is_a?(String)
544
+ doc = Nokogiri::XML(potential_file_path)
545
+ end
546
+ rescue
547
+ raise "%s Not an XML File" % potential_file_path
548
+ end
549
+
550
+ ret = Metalink4.new
551
+ ret.published = doc.at("metalink > published").content
552
+ ret.updated = doc.at("metalink > updated").content rescue nil
553
+ ret.origin = doc.at("metalink > origin").content rescue nil
554
+ ret.origin_dynamic = doc.at("metalink > origin[dynamic]").content == "true" rescue false
555
+
556
+ doc.search("metalink > file").each do |file|
557
+
558
+ ret.files << Metalink4File.new
559
+
560
+ ret.files.last.local_path = file.attr("name")
561
+
562
+ ret.files.last.copyright = file.at("copyright").content rescue nil
563
+ ret.files.last.description = file.at("description").content rescue nil
564
+ ret.files.last.identity = file.at("identity").content rescue nil
565
+ ret.files.last.language = file.at("language").content rescue nil
566
+ ret.files.last.logo = file.at("logo").content rescue nil
567
+ ret.files.last.os = file.at("os").content rescue nil
568
+ ret.files.last.publisher_name = file.at("publisher[name]").content rescue nil
569
+ ret.files.last.publisher_url = file.at("publisher[url]").content rescue nil
570
+ ret.files.last.signature = file.at("signature").content rescue nil
571
+ ret.files.last.version = file.at("version").content rescue nil
572
+
573
+ (file.search("hash") - file.search("pieces > hash")).each do |hash|
574
+ ret.files.last.hashes << Metalink4FileHash.new
575
+ ret.files.last.hashes.last.hash_value = hash.content rescue nil
576
+ ret.files.last.hashes.last.hash_type = hash.attr("type") rescue nil
577
+ end
578
+
579
+ ret.files.last.piece_size = file.at("pieces").attr("length").to_i rescue nil
580
+ piece_type = file.at("pieces").attr("type") rescue nil
581
+
582
+
583
+ ret.files.last.hashes ||= []
584
+ file.search("pieces > hash").each_with_index do |hash, hash_piece_index|
585
+ ret.files.last.hashes << Metalink4FileHash.new
586
+ ret.files.last.hashes.last.hash_value = (hash.content rescue nil)
587
+ ret.files.last.hashes.last.hash_type = piece_type
588
+ ret.files.last.hashes.last.piece = hash_piece_index
589
+ end
590
+
591
+ file.search("url").each do |url|
592
+ ret.files.last.urls << Metalink4FileUrl.new
593
+ ret.files.last.urls.last.url = url.content rescue nil
594
+ ret.files.last.urls.last.location = url.attr("location") rescue nil
595
+ ret.files.last.urls.last.priority = url.attr("priority") rescue nil
596
+ end
597
+
598
+ file.search("metaurl").each do |metaurl|
599
+ ret.files.last.urls << Metalink4FileUrl.new
600
+ ret.files.last.urls.last.url = metaurl.content rescue nil
601
+ ret.files.last.urls.last.priority = metaurl.attr("priority") rescue nil
602
+ end
603
+ end
604
+
605
+ ret
606
+ end
607
+ end