mini_tarball 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 +7 -0
- data/LICENSE.txt +21 -0
- data/lib/mini_tarball.rb +8 -0
- data/lib/mini_tarball/header.rb +57 -0
- data/lib/mini_tarball/header_formatter.rb +42 -0
- data/lib/mini_tarball/header_writer.rb +83 -0
- data/lib/mini_tarball/version.rb +5 -0
- data/lib/mini_tarball/write_only_stream.rb +13 -0
- data/lib/mini_tarball/writer.rb +95 -0
- metadata +136 -0
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.
|
data/lib/mini_tarball.rb
ADDED
@@ -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,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: []
|