mini_tarball 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 186593f6df9a85bfd1247d7ac48d2e2516d21d3dc7cd92e97774667ecc0768be
4
+ data.tar.gz: 7b4c0465dddd0c67822fca7f7d44c5e134dea5dae4e839f307975d42aacf5728
5
+ SHA512:
6
+ metadata.gz: 76239123207c12d071fa25ac28a7ee9e1d1796e3a68f15a5d124eeaf695590900130dff61cfe3e8af8c92f6b48ffa41e9be6e311ab0e93c42da5b0fb7efa1da8
7
+ data.tar.gz: e8bfcc562c8569b8f9168757406dfe162f31417a5bf7fb3b658b99a76de6896fa9b8dd3b5d73f8e1c33b434499dadc332a9c5dd6659185359a0d278d41b872c5
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2021 Discourse
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
13
+ all 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
21
+ THE SOFTWARE.
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'mini_tarball/header'
4
+ require 'mini_tarball/header_formatter'
5
+ require 'mini_tarball/header_writer'
6
+ require 'mini_tarball/write_only_stream'
7
+ require 'mini_tarball/writer'
8
+ require 'mini_tarball/version'
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MiniTarball
4
+ class Header
5
+ # Size of each block in the tar file in bytes
6
+ BLOCK_SIZE = 512 # bytes
7
+
8
+ TYPE_REGULAR = "0"
9
+ TYPE_LONG_LINK = "L"
10
+
11
+ # rubocop:disable Layout/HashAlignment
12
+ FIELDS = {
13
+ name: { length: 100, type: :chars },
14
+ mode: { length: 8, type: :number },
15
+ uid: { length: 8, type: :number },
16
+ gid: { length: 8, type: :number },
17
+ size: { length: 12, type: :number },
18
+ mtime: { length: 12, type: :number },
19
+ checksum: { length: 8, type: :checksum },
20
+ typeflag: { length: 1, type: :chars },
21
+ linkname: { length: 100, type: :chars },
22
+ magic: { length: 6, type: :chars },
23
+ version: { length: 2, type: :chars },
24
+ uname: { length: 32, type: :chars },
25
+ gname: { length: 32, type: :chars },
26
+ devmajor: { length: 8, type: :number },
27
+ devminor: { length: 8, type: :number },
28
+ prefix: { length: 155, type: :chars }
29
+ }
30
+ # rubocop:enable Layout/HashAlignment
31
+
32
+ def initialize(name:, mode: 0, uid: nil, gid: nil, size: 0, mtime: 0, typeflag: TYPE_REGULAR, linkname: "", uname: nil, gname: nil)
33
+ @values = {
34
+ name: name,
35
+ mode: mode,
36
+ uid: uid,
37
+ gid: gid,
38
+ size: size,
39
+ mtime: mtime.to_i,
40
+ checksum: nil,
41
+ typeflag: typeflag,
42
+ linkname: linkname,
43
+ magic: "ustar ",
44
+ version: " ",
45
+ uname: uname,
46
+ gname: gname,
47
+ devmajor: nil,
48
+ devminor: nil,
49
+ prefix: ""
50
+ }
51
+ end
52
+
53
+ def value_of(key)
54
+ @values[key]
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MiniTarball
4
+ ValueTooLargeError = Class.new(StandardError)
5
+
6
+ class HeaderFormatter
7
+ # @param value [Integer]
8
+ # @param length [Integer]
9
+ def self.format_number(value, length)
10
+ return nil if value.nil?
11
+ raise NotImplementedError.new("Negative numbers are not supported") if value.negative?
12
+
13
+ octal_length = length - 1
14
+ max_octal_value = ("0" + "7" * octal_length).to_i(8)
15
+
16
+ if (value <= max_octal_value)
17
+ to_octal(value, octal_length)
18
+ else
19
+ to_base256(value, length)
20
+ end
21
+ end
22
+
23
+ def self.to_octal(value, length)
24
+ "%0#{length}o" % value
25
+ end
26
+
27
+ def self.to_base256(value, length)
28
+ encoded = Array.new(length, 0)
29
+ encoded[0] = 0x80
30
+ index = length - 1
31
+
32
+ while value > 0
33
+ raise ValueTooLargeError.new("Value is too large: #{value}") if index == 0
34
+ encoded[index] = value % 256
35
+ value /= 256
36
+ index -= 1
37
+ end
38
+
39
+ encoded.pack("C#{length}")
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MiniTarball
4
+ class HeaderWriter
5
+ def initialize(io)
6
+ @io = io
7
+ end
8
+
9
+ def write(header)
10
+ write_long_name_header(header) if has_long_name?(header)
11
+ @io.write(to_binary(header))
12
+ end
13
+
14
+ private
15
+
16
+ def to_binary(header)
17
+ values_by_field = {}
18
+
19
+ Header::FIELDS.each do |name, field|
20
+ value = values_by_field[name] = header.value_of(name)
21
+
22
+ case field[:type]
23
+ when :number
24
+ values_by_field[name] = HeaderFormatter.format_number(value, field[:length])
25
+ when :checksum
26
+ values_by_field[name] = " " * field[:length]
27
+ end
28
+ end
29
+
30
+ update_checksum(values_by_field)
31
+ add_padding(encode(values_by_field.values))
32
+ end
33
+
34
+ def update_checksum(values_by_field)
35
+ checksum = encode(values_by_field.values).unpack("C*").sum
36
+ values_by_field[:checksum] = format_checksum(checksum)
37
+ end
38
+
39
+ def format_checksum(checksum)
40
+ length = Header::FIELDS[:checksum][:length] - 1
41
+ HeaderFormatter.format_number(checksum, length) << "\0 "
42
+ end
43
+
44
+ def encode(values)
45
+ @pack_format ||= Header::FIELDS.values
46
+ .map { |field| "a#{field[:length]}" }
47
+ .join("")
48
+
49
+ values.pack(@pack_format)
50
+ end
51
+
52
+ def add_padding(binary)
53
+ padding_length = (Header::BLOCK_SIZE - binary.length) % Header::BLOCK_SIZE
54
+ binary << "\0" * padding_length
55
+ end
56
+
57
+ def has_long_name?(header)
58
+ header.value_of(:name).bytesize > Header::FIELDS[:name][:length]
59
+ end
60
+
61
+ def write_long_name_header(header)
62
+ name = header.value_of(:name)
63
+ private_header = long_link_header(name, Header::TYPE_LONG_LINK)
64
+ data = [header.value_of(:name)].pack("Z*")
65
+
66
+ @io.write(to_binary(private_header))
67
+ @io.write(add_padding(data))
68
+ end
69
+
70
+ def long_link_header(name, type)
71
+ Header.new(
72
+ name: "././@LongLink",
73
+ mode: 0644,
74
+ uid: 0,
75
+ gid: 0,
76
+ size: name.bytesize + 1,
77
+ typeflag: type,
78
+ uname: "root",
79
+ gname: "root"
80
+ )
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MiniTarball
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MiniTarball
4
+ class WriteOnlyStream
5
+ def initialize(io)
6
+ @io = io
7
+ end
8
+
9
+ def write(data)
10
+ @io.write (data)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MiniTarball
4
+ class Writer
5
+ END_OF_TAR_BLOCK_SIZE = 1024
6
+
7
+ # @param [String] filename
8
+ # @yieldparam [Writer]
9
+ def self.create(filename)
10
+ File.open(filename, "wb") do |file|
11
+ use(file) { |writer| yield(writer) }
12
+ end
13
+ end
14
+
15
+ # @param [IO] io
16
+ # @yieldparam [Writer]
17
+ def self.use(io)
18
+ writer = new(io)
19
+
20
+ begin
21
+ yield(writer)
22
+ ensure
23
+ writer.close
24
+ end
25
+
26
+ nil
27
+ end
28
+
29
+ def initialize(io)
30
+ check_io!(io)
31
+
32
+ @io = io
33
+ @write_only_io = WriteOnlyStream.new(@io)
34
+ @header_writer = HeaderWriter.new(@write_only_io)
35
+ @closed = false
36
+ end
37
+
38
+ def add_file(name:, mode: 0644, uname: "nobody", gname: "nogroup", uid: nil, gid: nil, mtime: nil)
39
+ check_closed!
40
+
41
+ header_start_position = @io.pos
42
+ @header_writer.write(Header.new(name: name))
43
+
44
+ file_start_position = @io.pos
45
+ yield @write_only_io
46
+ file_size = @io.pos - file_start_position
47
+ write_padding
48
+
49
+ @io.seek(header_start_position)
50
+ @header_writer.write(Header.new(
51
+ name: name,
52
+ size: file_size,
53
+ mode: mode,
54
+ uid: uid,
55
+ gid: gid,
56
+ uname: uname,
57
+ gname: gname,
58
+ mtime: mtime || Time.now.utc
59
+ ))
60
+
61
+ @io.seek(0, IO::SEEK_END)
62
+ end
63
+
64
+ def closed?
65
+ @closed
66
+ end
67
+
68
+ def close
69
+ check_closed!
70
+
71
+ @io.write("\0" * END_OF_TAR_BLOCK_SIZE)
72
+ @io.close
73
+ @closed = true
74
+ end
75
+
76
+ private
77
+
78
+ def check_io!(io)
79
+ raise "No IO object given" unless io.respond_to?(:pos) &&
80
+ io.respond_to?(:seek) && io.respond_to?(:write) && io.respond_to?(:close)
81
+
82
+ io.seek(0, IO::SEEK_END)
83
+ raise "Stream must be empty" unless io.pos == 0
84
+ end
85
+
86
+ def check_closed!
87
+ raise IOError.new("#{self.class} is closed") if closed?
88
+ end
89
+
90
+ def write_padding
91
+ padding_length = (Header::BLOCK_SIZE - @io.pos) % Header::BLOCK_SIZE
92
+ @io.write("\0" * padding_length)
93
+ end
94
+ end
95
+ end
metadata ADDED
@@ -0,0 +1,136 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mini_tarball
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Discourse
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2021-02-22 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rake
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '13'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '13'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rubocop-discourse
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rubocop-rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: super_diff
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: simplecov
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ description:
98
+ email:
99
+ executables: []
100
+ extensions: []
101
+ extra_rdoc_files: []
102
+ files:
103
+ - LICENSE.txt
104
+ - lib/mini_tarball.rb
105
+ - lib/mini_tarball/header.rb
106
+ - lib/mini_tarball/header_formatter.rb
107
+ - lib/mini_tarball/header_writer.rb
108
+ - lib/mini_tarball/version.rb
109
+ - lib/mini_tarball/write_only_stream.rb
110
+ - lib/mini_tarball/writer.rb
111
+ homepage: https://github.com/discourse/mini_tarball
112
+ licenses:
113
+ - MIT
114
+ metadata:
115
+ homepage_uri: https://github.com/discourse/mini_tarball
116
+ source_code_uri: https://github.com/discourse/mini_tarball
117
+ post_install_message:
118
+ rdoc_options: []
119
+ require_paths:
120
+ - lib
121
+ required_ruby_version: !ruby/object:Gem::Requirement
122
+ requirements:
123
+ - - ">="
124
+ - !ruby/object:Gem::Version
125
+ version: 2.6.0
126
+ required_rubygems_version: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - ">="
129
+ - !ruby/object:Gem::Version
130
+ version: '0'
131
+ requirements: []
132
+ rubygems_version: 3.0.3
133
+ signing_key:
134
+ specification_version: 4
135
+ summary: A minimal implementation of the GNU Tar format.
136
+ test_files: []