1# coding: UTF-8
2
3require 'rubygems/package/tar_test_case'
4require 'rubygems/simple_gem'
5
6class TestGemPackage < Gem::Package::TarTestCase
7
8  def setup
9    super
10
11    @spec = quick_gem 'a' do |s|
12      s.description = 'π'
13      s.files = %w[lib/code.rb]
14    end
15
16    util_build_gem @spec
17
18    @gem = @spec.cache_file
19
20    @destination = File.join @tempdir, 'extract'
21
22    FileUtils.mkdir_p @destination
23  end
24
25  def test_class_new_old_format
26    open 'old_format.gem', 'wb' do |io|
27      io.write SIMPLE_GEM
28    end
29
30    package = Gem::Package.new 'old_format.gem'
31
32    assert package.spec
33  end
34
35  def test_add_checksums
36    gem_io = StringIO.new
37
38    spec = Gem::Specification.new 'build', '1'
39    spec.summary = 'build'
40    spec.authors = 'build'
41    spec.files = ['lib/code.rb']
42    spec.date = Time.at 0
43    spec.rubygems_version = Gem::Version.new '0'
44
45    FileUtils.mkdir 'lib'
46
47    open 'lib/code.rb', 'w' do |io|
48      io.write '# lib/code.rb'
49    end
50
51    package = Gem::Package.new spec.file_name
52    package.spec = spec
53    package.build_time = 1 # 0 uses current time
54    package.setup_signer
55
56    Gem::Package::TarWriter.new gem_io do |gem|
57      package.add_metadata gem
58      package.add_contents gem
59      package.add_checksums gem
60    end
61
62    gem_io.rewind
63
64    reader = Gem::Package::TarReader.new gem_io
65
66    checksums = nil
67    tar       = nil
68
69    reader.each_entry do |entry|
70      case entry.full_name
71      when 'checksums.yaml.gz' then
72        Zlib::GzipReader.wrap entry do |io|
73          checksums = io.read
74        end
75      when 'data.tar.gz' then
76        tar = entry.read
77      end
78    end
79
80    s = StringIO.new
81
82    package.gzip_to s do |io|
83      io.write spec.to_yaml
84    end
85
86    metadata_sha1   = Digest::SHA1.hexdigest s.string
87    metadata_sha512 = Digest::SHA512.hexdigest s.string
88
89    expected = {
90      'SHA1' => {
91        'metadata.gz' => metadata_sha1,
92        'data.tar.gz' => Digest::SHA1.hexdigest(tar),
93      },
94      'SHA512' => {
95        'metadata.gz' => metadata_sha512,
96        'data.tar.gz' => Digest::SHA512.hexdigest(tar),
97      }
98    }
99
100    assert_equal expected, YAML.load(checksums)
101  end
102
103  def test_add_files
104    spec = Gem::Specification.new
105    spec.files = %w[lib/code.rb lib/empty]
106
107    FileUtils.mkdir_p 'lib/empty'
108
109    open 'lib/code.rb',  'w' do |io| io.write '# lib/code.rb'  end
110    open 'lib/extra.rb', 'w' do |io| io.write '# lib/extra.rb' end
111
112    package = Gem::Package.new 'bogus.gem'
113    package.spec = spec
114
115    tar = util_tar do |tar_io|
116      package.add_files tar_io
117    end
118
119    tar.rewind
120
121    files = []
122
123    Gem::Package::TarReader.new tar do |tar_io|
124      tar_io.each_entry do |entry|
125        files << entry.full_name
126      end
127    end
128
129    assert_equal %w[lib/code.rb], files
130  end
131
132  def test_build
133    spec = Gem::Specification.new 'build', '1'
134    spec.summary = 'build'
135    spec.authors = 'build'
136    spec.files = ['lib/code.rb']
137    spec.rubygems_version = :junk
138
139    FileUtils.mkdir 'lib'
140
141    open 'lib/code.rb', 'w' do |io|
142      io.write '# lib/code.rb'
143    end
144
145    package = Gem::Package.new spec.file_name
146    package.spec = spec
147
148    package.build
149
150    assert_equal Gem::VERSION, spec.rubygems_version
151    assert_path_exists spec.file_name
152
153    reader = Gem::Package.new spec.file_name
154    assert_equal spec, reader.spec
155
156    assert_equal %w[metadata.gz data.tar.gz checksums.yaml.gz],
157                 reader.files
158
159    assert_equal %w[lib/code.rb], reader.contents
160  end
161
162  def test_build_auto_signed
163    FileUtils.mkdir_p File.join(Gem.user_home, '.gem')
164
165    private_key_path = File.join Gem.user_home, '.gem', 'gem-private_key.pem'
166    Gem::Security.write PRIVATE_KEY, private_key_path
167
168    public_cert_path = File.join Gem.user_home, '.gem', 'gem-public_cert.pem'
169    Gem::Security.write PUBLIC_CERT, public_cert_path
170
171    spec = Gem::Specification.new 'build', '1'
172    spec.summary = 'build'
173    spec.authors = 'build'
174    spec.files = ['lib/code.rb']
175
176    FileUtils.mkdir 'lib'
177
178    open 'lib/code.rb', 'w' do |io|
179      io.write '# lib/code.rb'
180    end
181
182    package = Gem::Package.new spec.file_name
183    package.spec = spec
184
185    package.build
186
187    assert_equal Gem::VERSION, spec.rubygems_version
188    assert_path_exists spec.file_name
189
190    reader = Gem::Package.new spec.file_name
191    assert reader.verify
192
193    assert_equal [PUBLIC_CERT.to_pem], reader.spec.cert_chain
194
195    assert_equal %w[metadata.gz       metadata.gz.sig
196                    data.tar.gz       data.tar.gz.sig
197                    checksums.yaml.gz checksums.yaml.gz.sig],
198                 reader.files
199
200    assert_equal %w[lib/code.rb], reader.contents
201  end
202
203  def test_build_invalid
204    spec = Gem::Specification.new 'build', '1'
205
206    package = Gem::Package.new spec.file_name
207    package.spec = spec
208
209    e = assert_raises Gem::InvalidSpecificationException do
210      package.build
211    end
212
213    assert_equal 'missing value for attribute summary', e.message
214  end
215
216  def test_build_signed
217    spec = Gem::Specification.new 'build', '1'
218    spec.summary = 'build'
219    spec.authors = 'build'
220    spec.files = ['lib/code.rb']
221    spec.cert_chain = [PUBLIC_CERT.to_pem]
222    spec.signing_key = PRIVATE_KEY
223
224    FileUtils.mkdir 'lib'
225
226    open 'lib/code.rb', 'w' do |io|
227      io.write '# lib/code.rb'
228    end
229
230    package = Gem::Package.new spec.file_name
231    package.spec = spec
232
233    package.build
234
235    assert_equal Gem::VERSION, spec.rubygems_version
236    assert_path_exists spec.file_name
237
238    reader = Gem::Package.new spec.file_name
239    assert reader.verify
240
241    assert_equal spec, reader.spec
242
243    assert_equal %w[metadata.gz       metadata.gz.sig
244                    data.tar.gz       data.tar.gz.sig
245                    checksums.yaml.gz checksums.yaml.gz.sig],
246                 reader.files
247
248    assert_equal %w[lib/code.rb], reader.contents
249  end
250
251  def test_contents
252    package = Gem::Package.new @gem
253
254    assert_equal %w[lib/code.rb], package.contents
255  end
256
257  def test_extract_files
258    package = Gem::Package.new @gem
259
260    package.extract_files @destination
261
262    extracted = File.join @destination, 'lib/code.rb'
263    assert_path_exists extracted
264
265    mask = 0100666 & (~File.umask)
266
267    assert_equal mask.to_s(8), File.stat(extracted).mode.to_s(8) unless
268      win_platform?
269  end
270
271  def test_extract_files_empty
272    data_tgz = util_tar_gz do end
273
274    gem = util_tar do |tar|
275      tar.add_file 'data.tar.gz', 0644 do |io|
276        io.write data_tgz.string
277      end
278
279      tar.add_file 'metadata.gz', 0644 do |io|
280        Zlib::GzipWriter.wrap io do |gzio|
281          gzio.write @spec.to_yaml
282        end
283      end
284    end
285
286    open 'empty.gem', 'wb' do |io|
287      io.write gem.string
288    end
289
290    package = Gem::Package.new 'empty.gem'
291
292    package.extract_files @destination
293
294    assert_path_exists @destination
295  end
296
297  def test_extract_tar_gz_absolute
298    package = Gem::Package.new @gem
299
300    tgz_io = util_tar_gz do |tar|
301      tar.add_file '/absolute.rb', 0644 do |io| io.write 'hi' end
302    end
303
304    e = assert_raises Gem::Package::PathError do
305      package.extract_tar_gz tgz_io, @destination
306    end
307
308    assert_equal("installing into parent path /absolute.rb of " +
309                 "#{@destination} is not allowed", e.message)
310  end
311
312  def test_install_location
313    package = Gem::Package.new @gem
314
315    file = 'file.rb'
316    file.taint
317
318    destination = package.install_location file, @destination
319
320    assert_equal File.join(@destination, 'file.rb'), destination
321    refute destination.tainted?
322  end
323
324  def test_install_location_absolute
325    package = Gem::Package.new @gem
326
327    e = assert_raises Gem::Package::PathError do
328      package.install_location '/absolute.rb', @destination
329    end
330
331    assert_equal("installing into parent path /absolute.rb of " +
332                 "#{@destination} is not allowed", e.message)
333  end
334
335  def test_install_location_extra_slash
336    skip 'no File.realpath on 1.8' if RUBY_VERSION < '1.9'
337    package = Gem::Package.new @gem
338
339    file = 'foo//file.rb'
340    file.taint
341
342    destination = @destination.sub '/', '//'
343
344    destination = package.install_location file, destination
345
346    assert_equal File.join(@destination, 'foo', 'file.rb'), destination
347    refute destination.tainted?
348  end
349
350  def test_install_location_relative
351    package = Gem::Package.new @gem
352
353    e = assert_raises Gem::Package::PathError do
354      package.install_location '../relative.rb', @destination
355    end
356
357    parent = File.expand_path File.join @destination, "../relative.rb"
358
359    assert_equal("installing into parent path #{parent} of " +
360                 "#{@destination} is not allowed", e.message)
361  end
362
363  def test_load_spec
364    entry = StringIO.new Gem.gzip @spec.to_yaml
365    def entry.full_name() 'metadata.gz' end
366
367    package = Gem::Package.new 'nonexistent.gem'
368
369    spec = package.load_spec entry
370
371    assert_equal @spec, spec
372  end
373
374  def test_verify
375    package = Gem::Package.new @gem
376
377    package.verify
378
379    assert_equal @spec, package.spec
380    assert_equal %w[checksums.yaml.gz data.tar.gz metadata.gz],
381                 package.files.sort
382  end
383
384  def test_verify_checksum_bad
385    data_tgz = util_tar_gz do |tar|
386      tar.add_file 'lib/code.rb', 0444 do |io|
387        io.write '# lib/code.rb'
388      end
389    end
390
391    data_tgz = data_tgz.string
392
393    gem = util_tar do |tar|
394      metadata_gz = Gem.gzip @spec.to_yaml
395
396      tar.add_file 'metadata.gz', 0444 do |io|
397        io.write metadata_gz
398      end
399
400      tar.add_file 'data.tar.gz', 0444 do |io|
401        io.write data_tgz
402      end
403
404      bogus_checksums = {
405        'SHA1' => {
406          'data.tar.gz' => 'bogus',
407          'metadata.gz' => 'bogus',
408        },
409      }
410      tar.add_file 'checksums.yaml.gz', 0444 do |io|
411        Zlib::GzipWriter.wrap io do |gz_io|
412          gz_io.write YAML.dump bogus_checksums
413        end
414      end
415    end
416
417    open 'mismatch.gem', 'wb' do |io|
418      io.write gem.string
419    end
420
421    package = Gem::Package.new 'mismatch.gem'
422
423    e = assert_raises Gem::Package::FormatError do
424      package.verify
425    end
426
427    assert_equal 'SHA1 checksum mismatch for data.tar.gz in mismatch.gem',
428                 e.message
429  end
430
431  def test_verify_checksum_missing
432    data_tgz = util_tar_gz do |tar|
433      tar.add_file 'lib/code.rb', 0444 do |io|
434        io.write '# lib/code.rb'
435      end
436    end
437
438    data_tgz = data_tgz.string
439
440    gem = util_tar do |tar|
441      metadata_gz = Gem.gzip @spec.to_yaml
442
443      tar.add_file 'metadata.gz', 0444 do |io|
444        io.write metadata_gz
445      end
446
447      digest = OpenSSL::Digest::SHA1.new
448      digest << metadata_gz
449
450      checksums = {
451        'SHA1' => {
452          'metadata.gz' => digest.hexdigest,
453        },
454      }
455
456      tar.add_file 'checksums.yaml.gz', 0444 do |io|
457        Zlib::GzipWriter.wrap io do |gz_io|
458          gz_io.write YAML.dump checksums
459        end
460      end
461
462      tar.add_file 'data.tar.gz', 0444 do |io|
463        io.write data_tgz
464      end
465    end
466
467    open 'data_checksum_missing.gem', 'wb' do |io|
468      io.write gem.string
469    end
470
471    package = Gem::Package.new 'data_checksum_missing.gem'
472
473    assert package.verify
474  end
475
476  def test_verify_corrupt
477    Tempfile.open 'corrupt' do |io|
478      data = Gem.gzip 'a' * 10
479      io.write tar_file_header('metadata.gz', "\000x", 0644, data.length)
480      io.write data
481      io.rewind
482
483      package = Gem::Package.new io.path
484
485      e = assert_raises Gem::Package::FormatError do
486        package.verify
487      end
488
489      assert_equal "tar is corrupt, name contains null byte in #{io.path}",
490                   e.message
491    end
492  end
493
494  def test_verify_empty
495    FileUtils.touch 'empty.gem'
496
497    package = Gem::Package.new 'empty.gem'
498
499    e = assert_raises Gem::Package::FormatError do
500      package.verify
501    end
502
503    assert_equal 'package metadata is missing in empty.gem', e.message
504  end
505
506  def test_verify_nonexistent
507    package = Gem::Package.new 'nonexistent.gem'
508
509    e = assert_raises Gem::Package::FormatError do
510      package.verify
511    end
512
513    assert_match %r%^No such file or directory%, e.message
514    assert_match %r%nonexistent.gem$%,           e.message
515  end
516
517  def test_verify_security_policy
518    package = Gem::Package.new @gem
519    package.security_policy = Gem::Security::HighSecurity
520
521    e = assert_raises Gem::Security::Exception do
522      package.verify
523    end
524
525    assert_equal 'unsigned gems are not allowed by the High Security policy',
526                 e.message
527
528    refute package.instance_variable_get(:@spec), '@spec must not be loaded'
529    assert_empty package.instance_variable_get(:@files), '@files must empty'
530  end
531
532  def test_verify_security_policy_low_security
533    @spec.cert_chain = [PUBLIC_CERT.to_pem]
534    @spec.signing_key = PRIVATE_KEY
535
536    FileUtils.mkdir_p 'lib'
537    FileUtils.touch 'lib/code.rb'
538
539    build = Gem::Package.new @gem
540    build.spec = @spec
541
542    build.build
543
544    package = Gem::Package.new @gem
545    package.security_policy = Gem::Security::LowSecurity
546
547    assert package.verify
548  end
549
550  def test_verify_security_policy_checksum_missing
551    @spec.cert_chain = [PUBLIC_CERT.to_pem]
552    @spec.signing_key = PRIVATE_KEY
553
554    build = Gem::Package.new @gem
555    build.spec = @spec
556    build.setup_signer
557
558    FileUtils.mkdir 'lib'
559    FileUtils.touch 'lib/code.rb'
560
561    open @gem, 'wb' do |gem_io|
562      Gem::Package::TarWriter.new gem_io do |gem|
563        build.add_metadata gem
564        build.add_contents gem
565
566        # write bogus data.tar.gz to foil signature
567        bogus_data = Gem.gzip 'hello'
568        gem.add_file_simple 'data.tar.gz', 0444, bogus_data.length do |io|
569          io.write bogus_data
570        end
571
572        # pre rubygems 2.0 gems do not add checksums
573      end
574    end
575
576    Gem::Security.trust_dir.trust_cert PUBLIC_CERT
577
578    package = Gem::Package.new @gem
579    package.security_policy = Gem::Security::HighSecurity
580
581    e = assert_raises Gem::Security::Exception do
582      package.verify
583    end
584
585    assert_equal 'invalid signature', e.message
586
587    refute package.instance_variable_get(:@spec), '@spec must not be loaded'
588    assert_empty package.instance_variable_get(:@files), '@files must empty'
589  end
590
591  def test_verify_truncate
592    open 'bad.gem', 'wb' do |io|
593      io.write File.read(@gem, 1024) # don't care about newlines
594    end
595
596    package = Gem::Package.new 'bad.gem'
597
598    e = assert_raises Gem::Package::FormatError do
599      package.verify
600    end
601
602    assert_equal 'package content (data.tar.gz) is missing in bad.gem',
603                 e.message
604  end
605
606  def test_spec
607    package = Gem::Package.new @gem
608
609    assert_equal @spec, package.spec
610  end
611
612  def util_tar
613    tar_io = StringIO.new
614
615    Gem::Package::TarWriter.new tar_io do |tar|
616      yield tar
617    end
618
619    tar_io.rewind
620
621    tar_io
622  end
623
624  def util_tar_gz(&block)
625    tar_io = util_tar(&block)
626
627    tgz_io = StringIO.new
628
629    # can't wrap TarWriter because it seeks
630    Zlib::GzipWriter.wrap tgz_io do |io| io.write tar_io.string end
631
632    StringIO.new tgz_io.string
633  end
634
635end
636
637