gzipped_tar 0.0.4 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.rubocop.yml +6 -0
- data/.travis.yml +4 -3
- data/README.md +1 -1
- data/gzipped_tar.gemspec +1 -1
- data/lib/gzipped_tar.rb +3 -0
- data/lib/gzipped_tar/reader.rb +1 -1
- data/lib/gzipped_tar/tar.rb +29 -0
- data/lib/gzipped_tar/tar/bounded_stream.rb +35 -0
- data/lib/gzipped_tar/tar/checksum_field.rb +19 -0
- data/lib/gzipped_tar/tar/digest_io.rb +60 -0
- data/lib/gzipped_tar/tar/entry.rb +122 -0
- data/lib/gzipped_tar/tar/field.rb +52 -0
- data/lib/gzipped_tar/tar/header.rb +161 -0
- data/lib/gzipped_tar/tar/reader.rb +119 -0
- data/lib/gzipped_tar/tar/restricted_stream.rb +21 -0
- data/lib/gzipped_tar/tar/split_name.rb +67 -0
- data/lib/gzipped_tar/tar/write_file.rb +42 -0
- data/lib/gzipped_tar/tar/write_signed_file.rb +73 -0
- data/lib/gzipped_tar/tar/writer.rb +135 -0
- data/lib/gzipped_tar/writer.rb +1 -1
- metadata +16 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: ca76d13e235befa915315db06a2cf6e319686dc848c6bfbbd627d87c9021c90c
|
4
|
+
data.tar.gz: 35e618c62c9c440b74b17b6fcd0c1157f0898ae4ec428156b0273f3a4ff11cee
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bea79c295ca72b0c62d7bdb63a295c37e415135397d0f133ec9d639164219fd2d5f750c158f3975289dad08c8b75e388bca396dfecc1c627b6ad33628713d570
|
7
|
+
data.tar.gz: 1afb1ce58a5ecb2a3181788cb13375ae171d6890600b92ae71d7e456ad9fa05b8f78bd51afbac76a888f5b0e38015e7b49b7a0d279c67ecfac8554a4bff41b71
|
data/.rubocop.yml
CHANGED
data/.travis.yml
CHANGED
data/README.md
CHANGED
data/gzipped_tar.gemspec
CHANGED
data/lib/gzipped_tar.rb
CHANGED
data/lib/gzipped_tar/reader.rb
CHANGED
@@ -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
|
data/lib/gzipped_tar/writer.rb
CHANGED
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
|
+
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:
|
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
|
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
|