gzipped_tar 0.0.4 → 0.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: c9020c4f3848255eb93c6dd596cfa4216346253e
4
- data.tar.gz: 41c20c89d72237fec73913c44810769ea61a7e6d
2
+ SHA256:
3
+ metadata.gz: ca76d13e235befa915315db06a2cf6e319686dc848c6bfbbd627d87c9021c90c
4
+ data.tar.gz: 35e618c62c9c440b74b17b6fcd0c1157f0898ae4ec428156b0273f3a4ff11cee
5
5
  SHA512:
6
- metadata.gz: cea6867c8e703e071716760e0d7c9f00239fbe6fca68b9a53325ee4b0d5c0fe98a0b75a29e66afb549f401573e6da1eb153005edc9e2016e79c9e6b2cb9caa7c
7
- data.tar.gz: 7f2a741f3c222085ac7b33112e5c0caa09270827971344e781d9476de92dd7e3b9462c6a9134a3ab4723df21ac12dd104e11003f79a0b58f3904054978b9a1d9
6
+ metadata.gz: bea79c295ca72b0c62d7bdb63a295c37e415135397d0f133ec9d639164219fd2d5f750c158f3975289dad08c8b75e388bca396dfecc1c627b6ad33628713d570
7
+ data.tar.gz: 1afb1ce58a5ecb2a3181788cb13375ae171d6890600b92ae71d7e456ad9fa05b8f78bd51afbac76a888f5b0e38015e7b49b7a0d279c67ecfac8554a4bff41b71
@@ -4,6 +4,12 @@ inherit_from:
4
4
  AllCops:
5
5
  TargetRubyVersion: 2.1
6
6
 
7
+ Performance/RedundantBlockCall:
8
+ Enabled: false
9
+
10
+ Style/AsciiComments:
11
+ Enabled: false
12
+
7
13
  Style/Documentation:
8
14
  Enabled: false
9
15
 
@@ -5,7 +5,8 @@ rvm:
5
5
  - 1.9.3
6
6
  - 2.0
7
7
  - 2.1.10
8
- - 2.2.8
9
- - 2.3.5
10
- - 2.4.2
8
+ - 2.2.9
9
+ - 2.3.6
10
+ - 2.4.3
11
+ - 2.5.0
11
12
  - jruby-9.1.14.0
data/README.md CHANGED
@@ -5,7 +5,7 @@ A simple interface for reading and writing gzipped tar files (.tar.gz) in memory
5
5
  ## Installation
6
6
 
7
7
  ```ruby
8
- gem "gzipped_tar", "~> 0.0.4"
8
+ gem "gzipped_tar", "~> 0.1.0"
9
9
  ```
10
10
 
11
11
  ## Usage
@@ -2,7 +2,7 @@
2
2
 
3
3
  Gem::Specification.new do |spec|
4
4
  spec.name = "gzipped_tar"
5
- spec.version = "0.0.4"
5
+ spec.version = "0.1.0"
6
6
  spec.authors = ["Pat Allan"]
7
7
  spec.email = ["pat@freelancing-gods.com"]
8
8
 
@@ -1,8 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "digest"
4
+
3
5
  module GZippedTar
4
6
  #
5
7
  end
6
8
 
9
+ require "gzipped_tar/tar"
7
10
  require "gzipped_tar/reader"
8
11
  require "gzipped_tar/writer"
@@ -20,7 +20,7 @@ module GZippedTar
20
20
  attr_reader :raw
21
21
 
22
22
  def reader
23
- Gem::Package::TarReader.new unzipped
23
+ GZippedTar::Tar::Reader.new unzipped
24
24
  end
25
25
 
26
26
  def unzipped
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GZippedTar
4
+ module Tar
5
+ ROW_WIDTH = 512
6
+ EMPTY_ROW = "\0" * ROW_WIDTH
7
+
8
+ Error = Class.new StandardError
9
+
10
+ FileOverflow = Class.new Error
11
+ NonSeekableIO = Class.new Error
12
+ TarInvalidError = Class.new Error
13
+ TooLongFileName = Class.new Error
14
+ UnexpectedEOF = Class.new Error
15
+ end
16
+ end
17
+
18
+ require "gzipped_tar/tar/bounded_stream"
19
+ require "gzipped_tar/tar/digest_io"
20
+ require "gzipped_tar/tar/entry"
21
+ require "gzipped_tar/tar/field"
22
+ require "gzipped_tar/tar/checksum_field"
23
+ require "gzipped_tar/tar/header"
24
+ require "gzipped_tar/tar/reader"
25
+ require "gzipped_tar/tar/restricted_stream"
26
+ require "gzipped_tar/tar/split_name"
27
+ require "gzipped_tar/tar/writer"
28
+ require "gzipped_tar/tar/write_file"
29
+ require "gzipped_tar/tar/write_signed_file"
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ #--
4
+ # Original source is copyright (C) 2004 Mauricio Julio Fernández Pradier
5
+ # This file has been copied and adapted to avoid reliance on differing
6
+ # behaviour across versions of rubygems, and to handle nil header values at
7
+ # the end of a file.
8
+ #++
9
+
10
+ class GZippedTar::Tar::BoundedStream
11
+ # Maximum number of bytes that can be written
12
+ attr_reader :limit
13
+
14
+ # Number of bytes written
15
+ attr_reader :written
16
+
17
+ # Wraps +io+ and allows up to +limit+ bytes to be written
18
+ def initialize(io, limit)
19
+ @io = io
20
+ @limit = limit
21
+ @written = 0
22
+ end
23
+
24
+ # Writes +data+ onto the IO, raising a FileOverflow exception if the
25
+ # number of bytes will be more than #limit
26
+ def write(data)
27
+ if data.bytesize + @written > @limit
28
+ raise GZippedTar::Tar::FileOverflow,
29
+ "You tried to feed more data than fits in the file."
30
+ end
31
+ @io.write data
32
+ @written += data.bytesize
33
+ data.bytesize
34
+ end
35
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ class GZippedTar::Tar::ChecksumField < GZippedTar::Tar::Field
4
+ EMPTY_VALUE = [" " * 8, " "].freeze
5
+
6
+ def pack
7
+ "a7a"
8
+ end
9
+
10
+ def unpack
11
+ "A8"
12
+ end
13
+
14
+ def to_s(value)
15
+ return EMPTY_VALUE if value.nil?
16
+
17
+ [format("%06o", value), " "]
18
+ end
19
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ #--
4
+ # Original source is from Rubygems.
5
+ # This file has been copied and adapted to avoid reliance on differing
6
+ # behaviour across versions of rubygems, and to handle nil header values at
7
+ # the end of a file.
8
+ #++
9
+
10
+ # IO wrapper that creates digests of contents written to the IO it wraps.
11
+ class GZippedTar::Tar::DigestIO
12
+ # Collected digests for wrapped writes.
13
+ #
14
+ # {
15
+ # 'SHA1' => #<OpenSSL::Digest: [...]>,
16
+ # 'SHA512' => #<OpenSSL::Digest: [...]>,
17
+ # }
18
+ attr_reader :digests
19
+
20
+ # Wraps +io+ and updates digest for each of the digest algorithms in
21
+ # the +digests+ Hash. Returns the digests hash. Example:
22
+ #
23
+ # io = StringIO.new
24
+ # digests = {
25
+ # 'SHA1' => OpenSSL::Digest.new('SHA1'),
26
+ # 'SHA512' => OpenSSL::Digest.new('SHA512'),
27
+ # }
28
+ #
29
+ # GZippedTar::Tar::DigestIO.wrap io, digests do |digest_io|
30
+ # digest_io.write "hello"
31
+ # end
32
+ #
33
+ # digests['SHA1'].hexdigest #=> "aaf4c61d[...]"
34
+ # digests['SHA512'].hexdigest #=> "9b71d224[...]"
35
+ def self.wrap(io, digests)
36
+ digest_io = new io, digests
37
+
38
+ yield digest_io
39
+
40
+ digests
41
+ end
42
+
43
+ # Creates a new DigestIO instance. Using ::wrap is recommended, see the
44
+ # ::wrap documentation for documentation of +io+ and +digests+.
45
+ def initialize(io, digests)
46
+ @io = io
47
+ @digests = digests
48
+ end
49
+
50
+ # Writes +data+ to the underlying IO and updates the digests
51
+ def write(data)
52
+ result = @io.write data
53
+
54
+ @digests.each_value do |digest|
55
+ digest << data
56
+ end
57
+
58
+ result
59
+ end
60
+ end
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ #++
4
+ # Original source is copyright (C) 2004 Mauricio Julio Fernández Pradier
5
+ # This file has been copied and adapted to avoid reliance on differing
6
+ # behaviour across versions of rubygems, and to handle nil header values at
7
+ # the end of a file.
8
+ #--
9
+
10
+ class GZippedTar::Tar::Entry
11
+ attr_reader :header
12
+
13
+ # Creates a new tar entry for +header+ that will be read from +io+
14
+ def initialize(header, io)
15
+ @closed = false
16
+ @header = header
17
+ @io = io
18
+ @orig_pos = @io.pos
19
+ @read = 0
20
+ end
21
+
22
+ def check_closed # :nodoc:
23
+ raise IOError, "closed #{self.class}" if closed?
24
+ end
25
+
26
+ # Number of bytes read out of the tar entry
27
+ def bytes_read
28
+ @read
29
+ end
30
+
31
+ # Closes the tar entry
32
+ def close
33
+ @closed = true
34
+ end
35
+
36
+ # Is the tar entry closed?
37
+ def closed?
38
+ @closed
39
+ end
40
+
41
+ # Are we at the end of the tar entry?
42
+ def eof?
43
+ check_closed
44
+
45
+ @read >= @header.size
46
+ end
47
+
48
+ # Full name of the tar entry
49
+ def full_name
50
+ if @header.prefix != ""
51
+ File.join @header.prefix, @header.name
52
+ else
53
+ @header.name
54
+ end
55
+ rescue ArgumentError => e
56
+ raise unless e.message == "string contains null byte"
57
+ raise GZippedTar::Tar::TarInvalidError,
58
+ "tar is corrupt, name contains null byte"
59
+ end
60
+
61
+ # Read one byte from the tar entry
62
+ def getc
63
+ check_closed
64
+
65
+ return nil if @read >= @header.size
66
+
67
+ ret = @io.getc
68
+ @read += 1 if ret
69
+
70
+ ret
71
+ end
72
+
73
+ # Is this tar entry a directory?
74
+ def directory?
75
+ @header.typeflag == "5"
76
+ end
77
+
78
+ # Is this tar entry a file?
79
+ def file?
80
+ @header.typeflag == "0"
81
+ end
82
+
83
+ # Is this tar entry a symlink?
84
+ def symlink?
85
+ @header.typeflag == "2"
86
+ end
87
+
88
+ # The position in the tar entry
89
+ def pos
90
+ check_closed
91
+
92
+ bytes_read
93
+ end
94
+
95
+ # Reads +len+ bytes from the tar file entry, or the rest of the entry if
96
+ # nil
97
+ def read(len = nil)
98
+ check_closed
99
+
100
+ return nil if @read >= @header.size
101
+
102
+ len ||= @header.size - @read
103
+ max_read = [len, @header.size - @read].min
104
+
105
+ ret = @io.read max_read
106
+ @read += ret.size
107
+
108
+ ret
109
+ end
110
+
111
+ alias readpartial read # :nodoc:
112
+
113
+ # Rewinds to the beginning of the tar file entry
114
+ def rewind
115
+ check_closed
116
+
117
+ raise GZippedTar::Tar::NonSeekableIO unless @io.respond_to? :pos=
118
+
119
+ @io.pos = @orig_pos
120
+ @read = 0
121
+ end
122
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ class GZippedTar::Tar::Field
4
+ attr_reader :name, :length, :octal, :required, :default
5
+
6
+ def initialize(name, length, options = {})
7
+ @name = name
8
+ @length = length
9
+ @octal = options[:octal]
10
+ @required = options[:required]
11
+ @default = options[:default]
12
+ end
13
+
14
+ def octal?
15
+ octal
16
+ end
17
+
18
+ def pack
19
+ return "a" if length == 1
20
+
21
+ "a#{length}"
22
+ end
23
+
24
+ def required?
25
+ required
26
+ end
27
+
28
+ def to_s(value)
29
+ return value unless octal?
30
+
31
+ if length == 2
32
+ format "%0#{length}o", value
33
+ else
34
+ format "%0#{length - 1}o", value
35
+ end
36
+ end
37
+
38
+ def translate(value)
39
+ value ||= default
40
+
41
+ return value unless octal? && value.is_a?(String)
42
+ return value.oct if value[/\A[0-7]*\z/]
43
+
44
+ raise ArgumentError, "#{value.inspect} is not an octal string"
45
+ end
46
+
47
+ def unpack
48
+ return "A" if length == 1
49
+
50
+ "A#{length}"
51
+ end
52
+ end
@@ -0,0 +1,161 @@
1
+ # frozen_string_literal: true
2
+
3
+ #++
4
+ # Original source is copyright (C) 2004 Mauricio Julio Fernández Pradier
5
+ # This file has been copied and adapted to avoid reliance on differing
6
+ # behaviour across versions of rubygems, and to handle nil header values at
7
+ # the end of a file.
8
+ #--
9
+
10
+ ##
11
+ #--
12
+ # struct tarfile_entry_posix {
13
+ # char name[100]; # ASCII + (Z unless filled)
14
+ # char mode[8]; # 0 padded, octal, null
15
+ # char uid[8]; # ditto
16
+ # char gid[8]; # ditto
17
+ # char size[12]; # 0 padded, octal, null
18
+ # char mtime[12]; # 0 padded, octal, null
19
+ # char checksum[8]; # 0 padded, octal, null, space
20
+ # char typeflag[1]; # file: "0" dir: "5"
21
+ # char linkname[100]; # ASCII + (Z unless filled)
22
+ # char magic[6]; # "ustar\0"
23
+ # char version[2]; # "00"
24
+ # char uname[32]; # ASCIIZ
25
+ # char gname[32]; # ASCIIZ
26
+ # char devmajor[8]; # 0 padded, octal, null
27
+ # char devminor[8]; # o padded, octal, null
28
+ # char prefix[155]; # ASCII + (Z unless filled)
29
+ # };
30
+ #++
31
+ # A header for a tar file
32
+
33
+ class GZippedTar::Tar::Header
34
+ Field = GZippedTar::Tar::Field
35
+ ChecksumField = GZippedTar::Tar::ChecksumField
36
+
37
+ # rubocop:disable Layout/ExtraSpacing
38
+ FIELDS = [
39
+ Field.new(:name, 100, :required => true),
40
+ Field.new(:mode, 8, :required => true, :octal => true),
41
+ Field.new(:uid, 8, :default => 0, :octal => true),
42
+ Field.new(:gid, 8, :default => 0, :octal => true),
43
+ Field.new(:size, 12, :required => true, :octal => true),
44
+ Field.new(:mtime, 12, :default => 0, :octal => true),
45
+ # Custom behaviour for the checksum:
46
+ ChecksumField.new(:checksum, 8, :octal => true),
47
+ # And back to normal:
48
+ Field.new(:typeflag, 1, :default => "0"),
49
+ Field.new(:linkname, 100),
50
+ Field.new(:magic, 6, :default => "ustar"),
51
+ Field.new(:version, 2, :default => "00", :octal => true),
52
+ Field.new(:uname, 32, :default => "wheel"),
53
+ Field.new(:gname, 32, :default => "wheel"),
54
+ Field.new(:devmajor, 8, :default => 0, :octal => true),
55
+ Field.new(:devminor, 8, :default => 0, :octal => true),
56
+ Field.new(:prefix, 155, :required => true)
57
+ ].freeze
58
+ # rubocop:enable Layout/ExtraSpacing
59
+
60
+ PACK_FORMAT = FIELDS.collect(&:pack).join("")
61
+ UNPACK_FORMAT = FIELDS.collect(&:unpack).join("")
62
+ HEADER_LENGTH = 512
63
+ BLANK = {
64
+ :name => "",
65
+ :size => "0",
66
+ :prefix => "",
67
+ :mode => "644",
68
+ :empty => true
69
+ }.freeze
70
+
71
+ # Creates a tar header from IO +stream+
72
+ def self.from(stream)
73
+ header = stream.read HEADER_LENGTH
74
+ return new(BLANK) if header.nil?
75
+
76
+ empty = (header == "\0" * HEADER_LENGTH)
77
+
78
+ new values_from_array(header.unpack(UNPACK_FORMAT)).merge(:empty => empty)
79
+ end
80
+
81
+ def self.values_from_array(values)
82
+ hash = {}
83
+
84
+ FIELDS.each_with_index do |field, index|
85
+ hash[field.name] = field.translate values[index]
86
+ end
87
+
88
+ hash
89
+ end
90
+
91
+ def initialize(values)
92
+ validate values
93
+
94
+ @empty = values.delete :empty
95
+ @values = FIELDS.inject({}) do |hash, field|
96
+ hash[field.name] = field.translate values[field.name]
97
+ hash
98
+ end
99
+
100
+ @values[:typeflag] = "0" if @values[:typeflag].empty?
101
+ end
102
+
103
+ def ==(other) # :nodoc:
104
+ other.is_a?(self.class) &&
105
+ FIELDS.all? { |field| values[field.name] == other.values[field.name] }
106
+ end
107
+
108
+ def empty?
109
+ @empty
110
+ end
111
+
112
+ def name
113
+ @values[:name]
114
+ end
115
+
116
+ def prefix
117
+ @values[:prefix]
118
+ end
119
+
120
+ def size
121
+ values[:size]
122
+ end
123
+
124
+ def to_s # :nodoc:
125
+ update_checksum
126
+ build_header
127
+ end
128
+
129
+ protected
130
+
131
+ attr_reader :values
132
+
133
+ private
134
+
135
+ def calculate_checksum(header)
136
+ header.unpack("C*").inject { |a, b| a + b }
137
+ end
138
+
139
+ def build_header
140
+ header = header_values.pack PACK_FORMAT
141
+
142
+ header + ("\0" * ((HEADER_LENGTH - header.size) % HEADER_LENGTH))
143
+ end
144
+
145
+ def header_values
146
+ FIELDS.collect { |field| field.to_s values[field.name] }.flatten
147
+ end
148
+
149
+ def update_checksum
150
+ values[:checksum] = nil
151
+ values[:checksum] = calculate_checksum(build_header)
152
+ end
153
+
154
+ def validate(values)
155
+ FIELDS.select(&:required?).each do |field|
156
+ next if values[field.name]
157
+
158
+ raise ArgumentError, ":name, :size, :prefix and :mode required"
159
+ end
160
+ end
161
+ end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ #--
4
+ # Original source is copyright (C) 2004 Mauricio Julio Fernández Pradier
5
+ # This file has been copied and adapted to avoid reliance on differing
6
+ # behaviour across versions of rubygems, and to handle nil header values at
7
+ # the end of a file.
8
+ #++
9
+
10
+ class GZippedTar::Tar::Reader
11
+ include Enumerable
12
+
13
+ ##
14
+ # Creates a new TarReader on +io+ and yields it to the block, if given.
15
+
16
+ def self.new(io)
17
+ reader = super
18
+
19
+ return reader unless block_given?
20
+
21
+ begin
22
+ yield reader
23
+ ensure
24
+ reader.close
25
+ end
26
+
27
+ nil
28
+ end
29
+
30
+ # Creates a new tar file reader on +io+ which needs to respond to #pos,
31
+ # #eof?, #read, #getc and #pos=
32
+ def initialize(io)
33
+ @io = io
34
+ @init_pos = io.pos
35
+ end
36
+
37
+ # Close the tar file
38
+ def close; end
39
+
40
+ # Iterates over files in the tarball yielding each entry
41
+ def each
42
+ return enum_for __method__ unless block_given?
43
+
44
+ until @io.eof?
45
+ header = GZippedTar::Tar::Header.from @io
46
+ return if header.empty?
47
+ entry = GZippedTar::Tar::Entry.new header, @io
48
+
49
+ yield entry
50
+
51
+ skip_past_entry header.size, entry.bytes_read
52
+ skip_past_trailing header.size
53
+
54
+ # make sure nobody can use #read, #getc or #rewind anymore
55
+ entry.close
56
+ end
57
+ end
58
+
59
+ alias each_entry each
60
+
61
+ # NOTE: Do not call #rewind during #each
62
+ def rewind
63
+ if @init_pos.zero?
64
+ raise GZippedTar::Tar::NonSeekableIO unless @io.respond_to? :rewind
65
+ @io.rewind
66
+ else
67
+ raise GZippedTar::Tar::NonSeekableIO unless @io.respond_to? :pos=
68
+ @io.pos = @init_pos
69
+ end
70
+ end
71
+
72
+ # Seeks through the tar file until it finds the +entry+ with +name+ and
73
+ # yields it. Rewinds the tar file to the beginning when the block
74
+ # terminates.
75
+ def seek(name)
76
+ found = find do |entry|
77
+ entry.full_name == name
78
+ end
79
+
80
+ return unless found
81
+
82
+ return yield found
83
+ ensure
84
+ rewind
85
+ end
86
+
87
+ private
88
+
89
+ attr_reader :io
90
+
91
+ def skip_past_entry(size, read)
92
+ # skip over the rest of the entry
93
+ pending = size - read
94
+
95
+ skip_by_seek
96
+ rescue Errno::EINVAL, NameError
97
+ skip_by_read pending
98
+ end
99
+
100
+ def skip_by_read(pending)
101
+ while pending > 0
102
+ bytes_read = io.read([pending, 4096].min).size
103
+ pending -= bytes_read
104
+ raise GZippedTar::Tar::UnexpectedEOF if io.eof? && !pending.zero?
105
+ end
106
+ end
107
+
108
+ def skip_by_seek
109
+ # avoid reading...
110
+ io.seek pending, IO::SEEK_CUR
111
+ end
112
+
113
+ def skip_past_trailing(size)
114
+ # discard trailing zeros
115
+ skip = (512 - (size % 512)) % 512
116
+
117
+ skip_by_read skip
118
+ end
119
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ #--
4
+ # Original source is copyright (C) 2004 Mauricio Julio Fernández Pradier
5
+ # This file has been copied and adapted to avoid reliance on differing
6
+ # behaviour across versions of rubygems, and to handle nil header values at
7
+ # the end of a file.
8
+ #++
9
+
10
+ # IO wrapper that provides only #write
11
+ class GZippedTar::Tar::RestrictedStream
12
+ # Creates a new RestrictedStream wrapping +io+
13
+ def initialize(io)
14
+ @io = io
15
+ end
16
+
17
+ # Writes +data+ onto the IO
18
+ def write(data)
19
+ @io.write data
20
+ end
21
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ class GZippedTar::Tar::SplitName
4
+ MAXIMUM_LENGTH = 256
5
+ MAXIMUM_NAME_LENGTH = 100
6
+ MAXIMUM_PATH_LENGTH = 155
7
+
8
+ def self.call(file)
9
+ new(file).call
10
+ end
11
+
12
+ def initialize(file)
13
+ @file = file
14
+ end
15
+
16
+ def call
17
+ return ["", file] unless split?
18
+
19
+ raise_if_too_long file, MAXIMUM_LENGTH, "path"
20
+ raise_if_too_long name, MAXIMUM_NAME_LENGTH, "name"
21
+ raise_if_too_long prefix, MAXIMUM_PATH_LENGTH, "base path"
22
+
23
+ [prefix, name]
24
+ end
25
+
26
+ private
27
+
28
+ attr_reader :file
29
+
30
+ def last_prefix_index
31
+ @last_prefix_index ||= begin
32
+ index = parts.length - 2
33
+ index -= 1 while parts[0..index].join("/").bytesize >= MAXIMUM_PATH_LENGTH
34
+ index
35
+ end
36
+ end
37
+
38
+ def name
39
+ parts[(last_prefix_index + 1)..-1].join("/")
40
+ end
41
+
42
+ def parts
43
+ @parts ||= file.split "/", -1
44
+ end
45
+
46
+ def prefix
47
+ parts[0..last_prefix_index].join("/")
48
+ end
49
+
50
+ def raise_if_too_long(string, maximum, description)
51
+ return if string.bytesize <= maximum
52
+
53
+ raise GZippedTar::Tar::TooLongFileName,
54
+ "File \"#{string}\" has a too long #{description} (should be " \
55
+ "#{maximum} or less)"
56
+ end
57
+
58
+ # If the file is less than MAXIMUM_NAME_LENGTH, it doesn't need to be split,
59
+ # and the prefix can be blank.
60
+ def split?
61
+ file.bytesize > MAXIMUM_NAME_LENGTH
62
+ end
63
+
64
+ def too_long?
65
+ file.bytesize > 256
66
+ end
67
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ class GZippedTar::Tar::WriteFile
4
+ def self.call(writer, io, name, mode, &block)
5
+ new(writer, io, name, mode).call(&block)
6
+ end
7
+
8
+ def initialize(writer, io, name, mode)
9
+ @writer = writer
10
+ @io = io
11
+ @name = name
12
+ @mode = mode
13
+ @initial = io.pos
14
+ end
15
+
16
+ def call(&block)
17
+ # placeholder for the header
18
+ io.write GZippedTar::Tar::EMPTY_ROW
19
+
20
+ block.call GZippedTar::Tar::RestrictedStream.new(io) if block
21
+
22
+ writer.pad_rows size
23
+ overwrite_header
24
+ end
25
+
26
+ private
27
+
28
+ attr_reader :writer, :io, :name, :mode, :initial
29
+
30
+ def overwrite_header
31
+ current_position = io.pos
32
+ io.pos = initial
33
+
34
+ writer.write_header name, mode, :size => size
35
+
36
+ io.pos = current_position
37
+ end
38
+
39
+ def size
40
+ @size ||= io.pos - initial - GZippedTar::Tar::ROW_WIDTH
41
+ end
42
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ class GZippedTar::Tar::WriteSignedFile
4
+ def self.call(writer, name, mode, signer, &block)
5
+ new(writer, name, mode, signer).call(&block)
6
+ end
7
+
8
+ def initialize(writer, name, mode, signer)
9
+ @writer = writer
10
+ @name = name
11
+ @mode = mode
12
+ @signer = signer
13
+ end
14
+
15
+ def call(&block)
16
+ add_file_digest(&block)
17
+
18
+ raise "no #{signer.digest_name} in #{digests.values.compact}" unless digest
19
+
20
+ write_signature if signer.key
21
+
22
+ digests
23
+ end
24
+
25
+ private
26
+
27
+ attr_reader :writer, :name, :mode, :signer
28
+
29
+ def add_file_digest(&block)
30
+ writer.add_file name, mode do |io|
31
+ GZippedTar::Tar::DigestIO.wrap io, digests, &block
32
+ end
33
+ end
34
+
35
+ def algorithms
36
+ [
37
+ signer.digest_algorithm,
38
+ Digest::SHA512
39
+ ].compact.uniq
40
+ end
41
+
42
+ def digest
43
+ @digest ||= digests.values.compact.detect do |digest|
44
+ digest_name(digest) == signer.digest_name
45
+ end
46
+ end
47
+
48
+ def digests
49
+ @digests ||= begin
50
+ digests = algorithms.collect(&:new).collect do |digest|
51
+ [digest_name(digest), digest]
52
+ end
53
+
54
+ Hash[*digests.flatten]
55
+ end
56
+ end
57
+
58
+ def digest_name(instance)
59
+ if instance.respond_to? :name
60
+ instance.name
61
+ else
62
+ instance.class.name[/::([^:]+)$/, 1]
63
+ end
64
+ end
65
+
66
+ def write_signature
67
+ signature = signer.sign digest.digest
68
+
69
+ writer.add_file_simple "#{name}.sig", 0o444, signature.length do |io|
70
+ io.write signature
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ #--
4
+ # Original source is copyright (C) 2004 Mauricio Julio Fernández Pradier
5
+ # This file has been copied and adapted to avoid reliance on differing
6
+ # behaviour across versions of rubygems, and to handle nil header values at
7
+ # the end of a file.
8
+ #++
9
+
10
+ class GZippedTar::Tar::Writer
11
+ ROW_WIDTH = GZippedTar::Tar::ROW_WIDTH
12
+
13
+ def self.new(io)
14
+ writer = super
15
+
16
+ return writer unless block_given?
17
+
18
+ begin
19
+ yield writer
20
+ ensure
21
+ writer.close
22
+ end
23
+
24
+ nil
25
+ end
26
+
27
+ # Creates a new TarWriter that will write to +io+
28
+ def initialize(io)
29
+ raise GZippedTar::Tar::NonSeekableIO unless io.respond_to? :pos=
30
+
31
+ @io = io
32
+ @closed = false
33
+ end
34
+
35
+ # Adds file +name+ with permissions +mode+, and yields an IO for writing the
36
+ # file to.
37
+ def add_file(name, mode, &block)
38
+ check_closed
39
+
40
+ GZippedTar::Tar::WriteFile.call self, io, name, mode, &block
41
+
42
+ self
43
+ end
44
+
45
+ # Adds +name+ with permissions +mode+ to the tar, yielding +io+ for writing
46
+ # the file. The +signer+ is used to add a digest file using its
47
+ # digest_algorithm per add_file_digest and a cryptographic signature in
48
+ # +name+.sig. If the signer has no key only the checksum file is added.
49
+ #
50
+ # Returns the digest.
51
+ def add_file_signed(name, mode, signer, &block)
52
+ GZippedTar::Tar::WriteSignedFile.call self, name, mode, signer, &block
53
+ end
54
+
55
+ # Add file +name+ with permissions +mode+ +size+ bytes long. Yields an IO
56
+ # to write the file to.
57
+ def add_file_simple(name, mode, size)
58
+ write_header name, mode, :size => size
59
+
60
+ stream = GZippedTar::Tar::BoundedStream.new io, size
61
+
62
+ yield stream if block_given?
63
+
64
+ min_padding = size - stream.written
65
+ io.write("\0" * min_padding)
66
+
67
+ pad_rows size
68
+
69
+ self
70
+ end
71
+
72
+ # Adds symlink +name+ with permissions +mode+, linking to +target+.
73
+ def add_symlink(name, target, mode)
74
+ write_header name, mode, :typeflag => "2", :linkname => target
75
+ end
76
+
77
+ # Closes the TarWriter
78
+ def close
79
+ check_closed
80
+
81
+ io.write GZippedTar::Tar::EMPTY_ROW
82
+ io.write GZippedTar::Tar::EMPTY_ROW
83
+ flush
84
+
85
+ @closed = true
86
+ end
87
+
88
+ # Is the TarWriter closed?
89
+ def closed?
90
+ @closed
91
+ end
92
+
93
+ # Flushes the TarWriter's IO
94
+ def flush
95
+ check_closed
96
+
97
+ io.flush if io.respond_to? :flush
98
+ end
99
+
100
+ # Creates a new directory in the tar file +name+ with +mode+
101
+ def mkdir(name, mode)
102
+ write_header name, mode, :typeflag => "5"
103
+ end
104
+
105
+ def pad_rows(size)
106
+ remainder = (ROW_WIDTH - (size % ROW_WIDTH)) % ROW_WIDTH
107
+
108
+ io.write "\0" * remainder
109
+ end
110
+
111
+ def write_header(name, mode, options = {})
112
+ check_closed
113
+
114
+ prefix, name = GZippedTar::Tar::SplitName.call name
115
+
116
+ io.write GZippedTar::Tar::Header.new({
117
+ :name => name,
118
+ :mode => mode,
119
+ :prefix => prefix,
120
+ :size => 0,
121
+ :mtime => Time.now
122
+ }.merge(options))
123
+
124
+ self
125
+ end
126
+
127
+ private
128
+
129
+ attr_reader :io
130
+
131
+ # Raises IOError if the TarWriter is closed
132
+ def check_closed
133
+ raise IOError, "closed #{self.class}" if closed?
134
+ end
135
+ end
@@ -32,7 +32,7 @@ module GZippedTar
32
32
  end
33
33
 
34
34
  def writer
35
- @writer ||= Gem::Package::TarWriter.new input_io
35
+ @writer ||= GZippedTar::Tar::Writer.new input_io
36
36
  end
37
37
  end
38
38
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gzipped_tar
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.4
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Pat Allan
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-12-14 00:00:00.000000000 Z
11
+ date: 2018-03-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -58,6 +58,19 @@ files:
58
58
  - gzipped_tar.gemspec
59
59
  - lib/gzipped_tar.rb
60
60
  - lib/gzipped_tar/reader.rb
61
+ - lib/gzipped_tar/tar.rb
62
+ - lib/gzipped_tar/tar/bounded_stream.rb
63
+ - lib/gzipped_tar/tar/checksum_field.rb
64
+ - lib/gzipped_tar/tar/digest_io.rb
65
+ - lib/gzipped_tar/tar/entry.rb
66
+ - lib/gzipped_tar/tar/field.rb
67
+ - lib/gzipped_tar/tar/header.rb
68
+ - lib/gzipped_tar/tar/reader.rb
69
+ - lib/gzipped_tar/tar/restricted_stream.rb
70
+ - lib/gzipped_tar/tar/split_name.rb
71
+ - lib/gzipped_tar/tar/write_file.rb
72
+ - lib/gzipped_tar/tar/write_signed_file.rb
73
+ - lib/gzipped_tar/tar/writer.rb
61
74
  - lib/gzipped_tar/writer.rb
62
75
  homepage: https://github.com/pat/gzipped_tar
63
76
  licenses:
@@ -79,7 +92,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
79
92
  version: '0'
80
93
  requirements: []
81
94
  rubyforge_project:
82
- rubygems_version: 2.6.13
95
+ rubygems_version: 2.7.6
83
96
  signing_key:
84
97
  specification_version: 4
85
98
  summary: In-memory reading/writing of .tar.gz files