tar 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tar/backports"
4
+ require "tar/error"
5
+ require "tar/schema"
6
+ require "tar/ustar"
7
+
8
+ module Tar
9
+ class Header
10
+ using Backports
11
+
12
+ SCHEMA = Schema.new {
13
+ string :name, 100
14
+ octal_number :mode, 8
15
+ octal_number :uid, 8
16
+ octal_number :gid, 8
17
+ octal_number :size, 12
18
+ timestamp :mtime, 12
19
+ octal_number :checksum, 8
20
+ string :typeflag, 1
21
+ string :link_name, 100
22
+ string :magic, 6
23
+ string :version, 2
24
+ string :uname, 32
25
+ string :gname, 32
26
+ octal_number :dev_major, 8
27
+ octal_number :dev_minor, 8
28
+ string :prefix, 155
29
+ }
30
+
31
+ def initialize(values, checksum: nil)
32
+ @values = values
33
+ check_checksum!(checksum) if checksum
34
+ end
35
+
36
+ SCHEMA.field_names.each do |name|
37
+ define_method name do
38
+ @values.fetch(name)
39
+ end
40
+ end
41
+
42
+ def path
43
+ return name if prefix.nil?
44
+ "#{prefix}/#{name}"
45
+ end
46
+
47
+ def self.parse(record)
48
+ expected_checksum = SCHEMA.clear(record, :checksum).chars.sum(&:ord)
49
+ new(SCHEMA.parse(record), checksum: expected_checksum)
50
+ end
51
+
52
+ private
53
+
54
+ def check_checksum!(expected_checksum)
55
+ raise ChecksumMismatch, "checksum mismatch at #{path.inspect}: expected #{expected_checksum}, got #{checksum}" unless checksum == expected_checksum
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tar/backports"
4
+ require "tar/error"
5
+ require "tar/header"
6
+ require "tar/ustar"
7
+
8
+ module Tar
9
+ class HeaderReader
10
+ using Backports
11
+
12
+ def initialize(io)
13
+ @io = io
14
+ end
15
+
16
+ def read
17
+ record = read_record
18
+
19
+ if empty?(record)
20
+ return nil if empty?(read_record)
21
+ raise InvalidArchive, "empty header"
22
+ end
23
+
24
+ Header.parse(record)
25
+ end
26
+
27
+ private
28
+
29
+ def read_record
30
+ USTAR.read_record(@io)
31
+ end
32
+
33
+ def empty?(record)
34
+ /\A\0+\z/m.match?(record)
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "zlib"
4
+
5
+ module Tar
6
+ module Polyfills
7
+ refine Zlib::GzipReader do
8
+ def read(length = nil, buffer = nil)
9
+ raise ArgumentError, "#{self.class} does not support read to buffer" unless buffer.nil?
10
+ super(length)
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tar/file_reader"
4
+ require "tar/header"
5
+ require "tar/header_reader"
6
+
7
+ module Tar
8
+ class Reader
9
+ include Enumerable
10
+
11
+ def initialize(io, **encoding_options)
12
+ @io = io
13
+ @encoding_options = encoding_options
14
+ @header_reader = HeaderReader.new(@io)
15
+ end
16
+
17
+ def each
18
+ return to_enum unless block_given?
19
+
20
+ loop do
21
+ header = @header_reader.read
22
+ break if header.nil?
23
+
24
+ file_reader = FileReader.new(header, @io, **@encoding_options)
25
+ yield file_reader
26
+ file_reader.skip_to_next_record
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tar
4
+ class Schema
5
+ def initialize(&block)
6
+ @fields = {}
7
+ @offset = 0
8
+ instance_eval(&block)
9
+ @fields.freeze
10
+
11
+ @unpack_format = @fields.values.map(&:unpack_format).join.freeze
12
+ end
13
+
14
+ def field_names
15
+ @fields.keys
16
+ end
17
+
18
+ def clear(record, field_name)
19
+ field = @fields.fetch(field_name)
20
+
21
+ record.dup.tap { |new_record|
22
+ new_record[field.offset, field.size] = " " * field.size
23
+ }
24
+ end
25
+
26
+ def parse(record)
27
+ @fields.zip(record.unpack(@unpack_format))
28
+ .map { |(name, type), value| [name, type.parse(value)] }
29
+ .to_h
30
+ end
31
+
32
+ def string(name, size)
33
+ add_field name, String, size
34
+ end
35
+
36
+ def octal_number(name, size)
37
+ add_field name, OctalNumber, size
38
+ end
39
+
40
+ def timestamp(name, size)
41
+ add_field name, Timestamp, size
42
+ end
43
+
44
+ private
45
+
46
+ def add_field(name, type, size)
47
+ @fields[name] = type.new(size, @offset)
48
+ @offset += size
49
+ end
50
+
51
+ class FieldType
52
+ attr_reader :size, :offset, :unpack_format
53
+
54
+ def initialize(size, offset, unpack_format:)
55
+ @size = size
56
+ @offset = offset
57
+ @unpack_format = unpack_format
58
+ end
59
+ end
60
+
61
+ class String < FieldType
62
+ def initialize(size, offset)
63
+ super(size, offset, unpack_format: "Z#{size}")
64
+ end
65
+
66
+ def parse(value)
67
+ value unless value.empty?
68
+ end
69
+ end
70
+
71
+ class OctalNumber < FieldType
72
+ def initialize(size, offset)
73
+ super(size, offset, unpack_format: "A#{size}")
74
+ end
75
+
76
+ def parse(value)
77
+ value.oct
78
+ end
79
+ end
80
+
81
+ class Timestamp < OctalNumber
82
+ def parse(value)
83
+ Time.at(super)
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tar/error"
4
+
5
+ module Tar
6
+ module USTAR
7
+ module_function
8
+
9
+ RECORD_SIZE = 512
10
+
11
+ def read_record(io)
12
+ record = io.read(RECORD_SIZE) || ""
13
+
14
+ raise UnexpectedEOF, "unexpected end-of-file: attempted to read #{RECORD_SIZE} bytes from #{io}, got #{record.size}" unless record.size == RECORD_SIZE
15
+
16
+ record
17
+ end
18
+
19
+ def records(file_size)
20
+ (file_size - 1) / RECORD_SIZE + 1
21
+ end
22
+
23
+ def records_size(file_size)
24
+ RECORD_SIZE * records(file_size)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tar
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path("../lib", __FILE__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+
6
+ require "tar/version"
7
+
8
+ Gem::Specification.new do |spec|
9
+ spec.name = "tar"
10
+ spec.version = Tar::VERSION
11
+ spec.authors = ["Andrew Haines"]
12
+ spec.email = ["andrew@haines.org.nz"]
13
+
14
+ spec.summary = "Read and write TAR files"
15
+ spec.homepage = "https://github.com/haines/tar"
16
+ spec.license = "MIT"
17
+
18
+ spec.files = `git ls-files -z`.split("\0").reject { |f| f.match(%r{^(test|spec|features)/}) }
19
+ spec.bindir = "exe"
20
+ spec.executables = spec.files.grep(%r{^#{spec.bindir}/}) { |f| File.basename(f) }
21
+ spec.require_paths = ["lib"]
22
+
23
+ spec.add_development_dependency "bundler", "~> 1.14"
24
+ spec.add_development_dependency "codeclimate-test-reporter", "~> 1.0.8"
25
+ spec.add_development_dependency "minitest", "~> 5.0"
26
+ spec.add_development_dependency "pry", "~> 0.10"
27
+ spec.add_development_dependency "rake", "~> 12.0"
28
+ end
metadata ADDED
@@ -0,0 +1,139 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: tar
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Andrew Haines
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2017-06-06 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.14'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.14'
27
+ - !ruby/object:Gem::Dependency
28
+ name: codeclimate-test-reporter
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 1.0.8
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 1.0.8
41
+ - !ruby/object:Gem::Dependency
42
+ name: minitest
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '5.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '5.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: pry
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '0.10'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '0.10'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rake
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '12.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '12.0'
83
+ description:
84
+ email:
85
+ - andrew@haines.org.nz
86
+ executables: []
87
+ extensions: []
88
+ extra_rdoc_files: []
89
+ files:
90
+ - ".gitignore"
91
+ - ".rubocop.yml"
92
+ - ".ruby-version"
93
+ - ".travis.yml"
94
+ - CODE_OF_CONDUCT.md
95
+ - Gemfile
96
+ - LICENSE.txt
97
+ - README.md
98
+ - Rakefile
99
+ - bin/console
100
+ - bin/setup
101
+ - lib/char_size.rb
102
+ - lib/tar.rb
103
+ - lib/tar/backports.rb
104
+ - lib/tar/error.rb
105
+ - lib/tar/file_reader.rb
106
+ - lib/tar/file_reader/line.rb
107
+ - lib/tar/header.rb
108
+ - lib/tar/header_reader.rb
109
+ - lib/tar/polyfills.rb
110
+ - lib/tar/reader.rb
111
+ - lib/tar/schema.rb
112
+ - lib/tar/ustar.rb
113
+ - lib/tar/version.rb
114
+ - tar.gemspec
115
+ homepage: https://github.com/haines/tar
116
+ licenses:
117
+ - MIT
118
+ metadata: {}
119
+ post_install_message:
120
+ rdoc_options: []
121
+ require_paths:
122
+ - lib
123
+ required_ruby_version: !ruby/object:Gem::Requirement
124
+ requirements:
125
+ - - ">="
126
+ - !ruby/object:Gem::Version
127
+ version: '0'
128
+ required_rubygems_version: !ruby/object:Gem::Requirement
129
+ requirements:
130
+ - - ">="
131
+ - !ruby/object:Gem::Version
132
+ version: '0'
133
+ requirements: []
134
+ rubyforge_project:
135
+ rubygems_version: 2.6.11
136
+ signing_key:
137
+ specification_version: 4
138
+ summary: Read and write TAR files
139
+ test_files: []