archiverb 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE.txt +19 -0
- data/README.md +102 -0
- data/lib/archiverb.rb +156 -0
- data/lib/archiverb/ar.rb +60 -0
- data/lib/archiverb/file.rb +76 -0
- data/lib/archiverb/stat.rb +55 -0
- data/lib/archiverb/tar.rb +214 -0
- data/lib/archiverb/version.rb +3 -0
- metadata +71 -0
data/LICENSE.txt
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Copyright (c) 2013 Caleb Crane
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a
|
4
|
+
copy of this software and associated documentation files (the "Software"),
|
5
|
+
to deal in the Software without restriction, including without limitation
|
6
|
+
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
7
|
+
and/or sell copies of the Software, and to permit persons to whom the
|
8
|
+
Software is furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included
|
11
|
+
in all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
14
|
+
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
15
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
16
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
17
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
18
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
19
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
Archiverb provides Ruby bindings for creating
|
2
|
+
[tar](http://en.wikipedia.org/wiki/Tar_(computing) and
|
3
|
+
[ar](http://en.wikipedia.org/wiki/Ar_(Unix) archives in memory.
|
4
|
+
|
5
|
+
## Install
|
6
|
+
|
7
|
+
``gem install archiverb``
|
8
|
+
|
9
|
+
## Use
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
require "archiverb/ar"
|
13
|
+
require "archiverb/tar"
|
14
|
+
```
|
15
|
+
|
16
|
+
## Adding files from the file system
|
17
|
+
|
18
|
+
```ruby
|
19
|
+
archive = Archiverb::Ar.new(File.expand_path("../henryIV.ar", __FILE__))
|
20
|
+
archive.add(File.expand_path("../spec/data/heneryIV.txt", __FILE__))
|
21
|
+
archive.add(File.expand_path("../spec/data/heneryIV-westmoreland.txt", __FILE__))
|
22
|
+
|
23
|
+
# archive will be written to henryIV.ar
|
24
|
+
archive.write
|
25
|
+
```
|
26
|
+
|
27
|
+
## Read an archive from the file system
|
28
|
+
|
29
|
+
```ruby
|
30
|
+
archive = Archiverb::Ar.new(File.expand_path("../henryIV.ar", __FILE__))
|
31
|
+
archive.read
|
32
|
+
|
33
|
+
archive.names # => ["heneryIV.txt", "heneryIV-westmoreland.txt"]
|
34
|
+
archive.files # => [#<Archiverb::File:0x007f8d7b90acf8 @name="heneryIV.txt" ... >, ...]
|
35
|
+
```
|
36
|
+
|
37
|
+
## Adding files from memory
|
38
|
+
|
39
|
+
```ruby
|
40
|
+
archive = Archiverb::Ar.new(File.expand_path("../henryIV.ar", __FILE__))
|
41
|
+
|
42
|
+
contents = IO.read((File.expand_path("../spec/data/heneryIV.txt", __FILE__)))
|
43
|
+
archive.add("henryIV.txt", contents)
|
44
|
+
|
45
|
+
contents = IO.read((File.expand_path("../spec/data/heneryIV-westmoreland.txt", __FILE__)))
|
46
|
+
archive.add("henryIV-westmoreland.txt", contents)
|
47
|
+
|
48
|
+
archive.write
|
49
|
+
```
|
50
|
+
|
51
|
+
|
52
|
+
```ruby
|
53
|
+
archive = Archiverb::Tar.new(File.expand_path("../henryIV.tar", __FILE__))
|
54
|
+
|
55
|
+
archive.add("data/", :mode => 0744)
|
56
|
+
|
57
|
+
contents = IO.read((File.expand_path("../spec/data/heneryIV.txt", __FILE__)))
|
58
|
+
archive.add("data/henryIV.txt", contents)
|
59
|
+
|
60
|
+
contents = IO.read((File.expand_path("../spec/data/heneryIV-westmoreland.txt", __FILE__)))
|
61
|
+
archive.add("data/henryIV-westmoreland.txt", contents)
|
62
|
+
|
63
|
+
archive.write
|
64
|
+
```
|
65
|
+
|
66
|
+
## Working with Gzip Files
|
67
|
+
|
68
|
+
### Writing to a Gzip file
|
69
|
+
|
70
|
+
To create a gzipped tar archive, populate a ``Archiverb::Tar`` object in
|
71
|
+
memory then create a ``GzipWriter`` object and pass it to
|
72
|
+
``Archiverb::Tar#write``.
|
73
|
+
|
74
|
+
```ruby
|
75
|
+
require "zlib"
|
76
|
+
|
77
|
+
path = File.expand_path("../henryIV.tar", __FILE__)
|
78
|
+
|
79
|
+
archive = Archiverb::Tar.new
|
80
|
+
archive.add("data/henryIV.txt",
|
81
|
+
IO.read((File.expand_path("../spec/data/heneryIV.txt", __FILE__))))
|
82
|
+
archive.add("data/henryIV-westmoreland.txt",
|
83
|
+
IO.read((File.expand_path("../spec/data/heneryIV-westmoreland.txt", __FILE__))))
|
84
|
+
|
85
|
+
Zlib::GzipWriter.open(path) do |gz|
|
86
|
+
archive.write(gz)
|
87
|
+
end
|
88
|
+
```
|
89
|
+
|
90
|
+
|
91
|
+
### Reading from a Gzip file
|
92
|
+
|
93
|
+
```ruby
|
94
|
+
require "zlib"
|
95
|
+
|
96
|
+
File.open(File.expand_path("../henryIV.tgz", __FILE__)) do |f|
|
97
|
+
gz = Zlib::GzipReader.new(f)
|
98
|
+
archive = Archiverb::Tar.new(gz)
|
99
|
+
archive.read
|
100
|
+
archive.names # => ["data/henryIV.txt", "data/henryIV-westmoreland.txt"]
|
101
|
+
end
|
102
|
+
```
|
data/lib/archiverb.rb
ADDED
@@ -0,0 +1,156 @@
|
|
1
|
+
require "archiverb/stat"
|
2
|
+
require "archiverb/file"
|
3
|
+
|
4
|
+
# Provides a common interface for working with different types of
|
5
|
+
# archive formats.
|
6
|
+
#
|
7
|
+
# @example
|
8
|
+
# arc = Archiverb::Ar.new("junk.ar", "a.txt", "b.txt", "c.txt")
|
9
|
+
# arc.write("/tmp/junk.ar") # => creates /tmp/junk.ar with {a,b,c}.txt
|
10
|
+
class Archiverb
|
11
|
+
module Error; end
|
12
|
+
class StandarError < ::StandardError; include Error; end
|
13
|
+
class ArgumentError < ::ArgumentError; include Error; end
|
14
|
+
class InvalidFormat < StandardError; end
|
15
|
+
class WrongChksum < StandardError; end
|
16
|
+
class AbstractMethod < StandardError; end
|
17
|
+
|
18
|
+
include Enumerable
|
19
|
+
|
20
|
+
def initialize(path_or_io = nil, *files, &blk)
|
21
|
+
raise NotImplementedError if self.class == Archiverb
|
22
|
+
@opts = files.last.is_a?(Hash) ? files.pop : {}
|
23
|
+
files.each { |file| add(file) }
|
24
|
+
if block_given?
|
25
|
+
r, w = IO.pipe
|
26
|
+
@source = lambda do
|
27
|
+
blk.call(w)
|
28
|
+
w.close unless w.closed?
|
29
|
+
r
|
30
|
+
end
|
31
|
+
else
|
32
|
+
@source = lambda { path_or_io.is_a?(String) ? ::File.new(path_or_io, "a+").tap{|f| f.rewind } : path_or_io }
|
33
|
+
end
|
34
|
+
@out = path_or_io
|
35
|
+
@files = {}
|
36
|
+
end
|
37
|
+
|
38
|
+
def path
|
39
|
+
@out
|
40
|
+
end
|
41
|
+
|
42
|
+
def files
|
43
|
+
@files.values
|
44
|
+
end
|
45
|
+
|
46
|
+
def [](file)
|
47
|
+
@files[file]
|
48
|
+
end
|
49
|
+
|
50
|
+
# Iterate over each file.
|
51
|
+
# @yield { |name, file| }
|
52
|
+
def each(&blk)
|
53
|
+
@files.each(&blk)
|
54
|
+
end
|
55
|
+
|
56
|
+
def names
|
57
|
+
@files.keys
|
58
|
+
end
|
59
|
+
|
60
|
+
def count
|
61
|
+
@files.keys.length
|
62
|
+
end
|
63
|
+
|
64
|
+
# Pulls each file out of the archive and places them in #files.
|
65
|
+
# @todo take a block and yield each file as it's read without storing it in #files
|
66
|
+
def read
|
67
|
+
return self if @source.nil?
|
68
|
+
io = @source.call
|
69
|
+
io.respond_to?(:binmode) && io.binmode
|
70
|
+
preprocess(io)
|
71
|
+
while (header = next_header(io))
|
72
|
+
@files[header[:name]] = File.new(header[:name], read_file(header, io), Stat.new(header))
|
73
|
+
end
|
74
|
+
io.close
|
75
|
+
self
|
76
|
+
end
|
77
|
+
|
78
|
+
# Add a file to the archive.
|
79
|
+
# @param [String, File, IO]
|
80
|
+
# @param [Hash] opts options to pass to Stat.new
|
81
|
+
# @param [IO, ::File, String, StringIO] io
|
82
|
+
def add(name, opts = {}, io = nil, &blk)
|
83
|
+
if block_given?
|
84
|
+
@files[name] = File.new(name, *IO.pipe, &blk)
|
85
|
+
return self
|
86
|
+
end
|
87
|
+
|
88
|
+
if io
|
89
|
+
opts, io = io, opts if io.is_a?(Hash)
|
90
|
+
elsif !opts.is_a?(Hash)
|
91
|
+
opts, io = {}, opts
|
92
|
+
end
|
93
|
+
|
94
|
+
if io
|
95
|
+
if io.is_a?(String) || io.is_a?(StringIO) || io.is_a?(IO) || io.is_a?(::File)
|
96
|
+
@files[name] = File.new(name, io, Stat.new(io, opts))
|
97
|
+
else
|
98
|
+
raise ArgumentError.new("unsupported data source: #{io.class}")
|
99
|
+
end
|
100
|
+
else
|
101
|
+
case name
|
102
|
+
when String
|
103
|
+
fio = ::File.exists?(name) ? ::File.new(name, "r") : ""
|
104
|
+
@files[name] = File.new(name, fio, Stat.new(fio, opts))
|
105
|
+
when ::File
|
106
|
+
@files[name.path] = File.new(name.path, name, Stat.new(name, opts))
|
107
|
+
else
|
108
|
+
opts[:name] = name.respond_to?(:path) ? name.path : name.__id__.to_s if opts[:name].nil?
|
109
|
+
@files[opts[:name]] = File.new(opts[:name], name, Stat.new(name, opts))
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
self
|
114
|
+
end
|
115
|
+
|
116
|
+
def write(path = @out, &blk)
|
117
|
+
if block_given?
|
118
|
+
# use a pipe instead?
|
119
|
+
yield StringIO.new.tap { |io| write_to(io) }.string
|
120
|
+
elsif path.is_a?(String)
|
121
|
+
::File.open(path, "w") { |io| write_to(io) }
|
122
|
+
else
|
123
|
+
path.respond_to?(:truncate) && path.truncate
|
124
|
+
write_to(path)
|
125
|
+
end
|
126
|
+
self
|
127
|
+
end
|
128
|
+
|
129
|
+
private
|
130
|
+
|
131
|
+
|
132
|
+
# Abstract method
|
133
|
+
# Write all files in the archive, in the archive format, to the given IO
|
134
|
+
# @return [Hash] must have :name, :mtime, :uid, :gid, and mode
|
135
|
+
def write_io(io)
|
136
|
+
raise AbstractMethod
|
137
|
+
end
|
138
|
+
|
139
|
+
# Abstract method
|
140
|
+
# Get the next header for the next file
|
141
|
+
def next_header(io)
|
142
|
+
raise AbstractMethod
|
143
|
+
end
|
144
|
+
|
145
|
+
# Abstract method
|
146
|
+
# Perform any preprocessing and validation on the archive.
|
147
|
+
# Inheriting classes aren't requried to implement this method.
|
148
|
+
def preprocess(io)
|
149
|
+
end
|
150
|
+
|
151
|
+
# Abstract method
|
152
|
+
# Given a file header and an IO that is the archive retrieve the file.
|
153
|
+
def read_file(header, io)
|
154
|
+
raise AbstractMethod
|
155
|
+
end
|
156
|
+
end # class::Archiverb
|
data/lib/archiverb/ar.rb
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'archiverb'
|
2
|
+
|
3
|
+
class Archiverb
|
4
|
+
class Ar < Archiverb
|
5
|
+
|
6
|
+
private
|
7
|
+
|
8
|
+
def write_to(io)
|
9
|
+
io.write("!<arch>\n")
|
10
|
+
@files.each do |_, file|
|
11
|
+
normal = [:name, :mtime, :uid, :gid, :mode].inject({}){|n,k| n.tap{ n[k] = file.send(k) } }
|
12
|
+
normal[:mtime] = normal[:mtime].to_i
|
13
|
+
normal[:raw] = file.read
|
14
|
+
if normal[:name].length > 16
|
15
|
+
normal[:name] = "#1/#{file.name.length + 3}"
|
16
|
+
normal[:raw] = "#{file.name}\0\0\0" + normal[:raw]
|
17
|
+
normal[:size] = file.name.length + 3
|
18
|
+
end
|
19
|
+
normal[:size] = normal[:raw].length
|
20
|
+
printf(io, "%-16s%-12u%-6d%-6d%-8o%-10u`\n", *[:name, :mtime, :uid, :gid, :mode, :size].map{|k| normal[k]})
|
21
|
+
io.write(normal[:raw])
|
22
|
+
io.write("\n") if io.pos % 2 == 1
|
23
|
+
end
|
24
|
+
io.close
|
25
|
+
self
|
26
|
+
end
|
27
|
+
|
28
|
+
def next_header(io)
|
29
|
+
return nil if io.eof?
|
30
|
+
io.read(1) if io.pos % 2 == 1
|
31
|
+
header = {}
|
32
|
+
header[:name] = io.read(16) || (return nil)
|
33
|
+
header[:name].strip!
|
34
|
+
header[:mtime] = io.read(12) || (return nil)
|
35
|
+
header[:uid] = io.read(6).to_i
|
36
|
+
header[:gid] = io.read(6).to_i
|
37
|
+
header[:mode] = io.read(8).to_i(8)
|
38
|
+
header[:size] = io.read(10).to_i
|
39
|
+
header[:magic] = io.read(2)
|
40
|
+
raise InvalidFormat unless header[:magic] == "`\n"
|
41
|
+
if header[:name][0..2] == "#1/"
|
42
|
+
# bsd format extended file name
|
43
|
+
header[:name] = io.read(header[:name][3..-1].to_i)
|
44
|
+
header[:size] -= header[:name].length
|
45
|
+
header[:name] = header[:name][0..-4]
|
46
|
+
# @todo support gnu format for extended file name
|
47
|
+
end
|
48
|
+
header
|
49
|
+
end # next_header(io)
|
50
|
+
|
51
|
+
def preprocess(io)
|
52
|
+
raise InvalidFormat unless io.read(8) == "!<arch>\n"
|
53
|
+
end
|
54
|
+
|
55
|
+
def read_file(header, io)
|
56
|
+
io.read(header[:size])
|
57
|
+
end
|
58
|
+
|
59
|
+
end # class::Ar
|
60
|
+
end # class::Archiverb
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require "stringio"
|
2
|
+
|
3
|
+
class Archiverb
|
4
|
+
class File
|
5
|
+
# the basename of the file
|
6
|
+
attr_reader :name
|
7
|
+
# the directory path leading to the file
|
8
|
+
attr_reader :dir
|
9
|
+
# the path and name of the file
|
10
|
+
attr_reader :path
|
11
|
+
# octal mode
|
12
|
+
attr_accessor :mode
|
13
|
+
# The user id and group id of the file. Set #stat.uname and #stat.gname
|
14
|
+
# to force ownership by name rather than id.
|
15
|
+
attr_reader :uid, :gid
|
16
|
+
# @return [Time] modification time
|
17
|
+
attr_reader :mtime
|
18
|
+
|
19
|
+
attr_reader :size
|
20
|
+
alias :bytes :size
|
21
|
+
|
22
|
+
# the raw io object, you can add to it prior to calling read
|
23
|
+
attr_reader :io
|
24
|
+
# [Archiverb::Stat]
|
25
|
+
attr_reader :stat
|
26
|
+
|
27
|
+
def initialize(name, io, buff=io, stat=nil, &blk)
|
28
|
+
buff,stat = stat, buff if buff.is_a?(Stat) || buff.is_a?(::File::Stat)
|
29
|
+
stat = Stat.new(io) if stat.nil?
|
30
|
+
|
31
|
+
@name = ::File.basename(name)
|
32
|
+
@dir = ::File.dirname(name)
|
33
|
+
@path = name
|
34
|
+
@mtime = stat.mtime.is_a?(Fixnum) ? Time.at(stat.mtime) : stat.mtime
|
35
|
+
@uid = stat.uid
|
36
|
+
@gid = stat.gid
|
37
|
+
@mode = stat.mode
|
38
|
+
@size = stat.size
|
39
|
+
|
40
|
+
@readbuff = buff
|
41
|
+
@readback = blk unless blk.nil?
|
42
|
+
@io = io.is_a?(String) ? StringIO.new(io) : io
|
43
|
+
@io.binmode
|
44
|
+
if @io.respond_to?(:path) && ::File.directory?(@io.path)
|
45
|
+
@size = 0
|
46
|
+
end
|
47
|
+
@stat = stat
|
48
|
+
end # initialize(io, stat)
|
49
|
+
|
50
|
+
def read
|
51
|
+
return @raw if @raw
|
52
|
+
|
53
|
+
if @readback && @readbuff
|
54
|
+
@readback.call(@readbuff)
|
55
|
+
@readbuff.close_write
|
56
|
+
end
|
57
|
+
|
58
|
+
@io.rewind unless @stat.pipe?
|
59
|
+
|
60
|
+
if @io.respond_to?(:path) && ::File.directory?(@io.path)
|
61
|
+
@raw = ""
|
62
|
+
else
|
63
|
+
@raw = @io.read
|
64
|
+
@size = @raw.length
|
65
|
+
end
|
66
|
+
@io.close
|
67
|
+
@raw
|
68
|
+
end # read
|
69
|
+
|
70
|
+
# Prevents future access to the contents of the file and hopefully frees up memory.
|
71
|
+
def close
|
72
|
+
@raw = nil
|
73
|
+
end # close
|
74
|
+
|
75
|
+
end # class::File
|
76
|
+
end # class::Archiverb
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require "ostruct"
|
2
|
+
class Archiverb
|
3
|
+
class Stat < OpenStruct
|
4
|
+
@@reqdatrs = [ :dev , :dev_major , :dev_minor , :ino , :mode , :nlink ,
|
5
|
+
:gid , :uid , :rdev_major , :rdev_minor , :size , :blksize ,
|
6
|
+
:blocks , :atime , :mtime , :ctime , :ftype , :pipe? ,
|
7
|
+
:rdev , :symlink?
|
8
|
+
]
|
9
|
+
def initialize(io, start = {})
|
10
|
+
return super(Hash[@@reqdatrs.map{|m| [m, def_v(m)]}].merge(io)) if io.is_a?(Hash)
|
11
|
+
return super(stat_hash(io).merge(start)) if io.is_a?(::File::Stat)
|
12
|
+
|
13
|
+
statm = [:lstat, :stat].find{|m| io.respond_to?(m)}
|
14
|
+
return super(Hash[@@reqdatrs.map{|m| [m, def_v(m)]}].merge(stat_hash(io)).merge(start)) if statm.nil?
|
15
|
+
|
16
|
+
hash = stat_hash(io.send(statm))
|
17
|
+
hash[:readlink] = ::File.readlink(io) if hash[:symlink?]
|
18
|
+
return super(hash.merge(start))
|
19
|
+
end
|
20
|
+
|
21
|
+
# ASCII representation of the owner and group of the file respectively.
|
22
|
+
# In TAR, if found, the user and group IDs are used rather than the values
|
23
|
+
# in the uid and gid fields.
|
24
|
+
attr_accessor :uname, :gname
|
25
|
+
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def def_v(attr)
|
30
|
+
case attr
|
31
|
+
when :atime, :ctime, :mtime
|
32
|
+
Time.new
|
33
|
+
when :size
|
34
|
+
0
|
35
|
+
when :gid
|
36
|
+
Process.egid
|
37
|
+
when :uid
|
38
|
+
Process.euid
|
39
|
+
when :mode
|
40
|
+
0644
|
41
|
+
when :ftype
|
42
|
+
"file"
|
43
|
+
else
|
44
|
+
false
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def stat_hash(stat)
|
49
|
+
@@reqdatrs.inject({}) do |h , meth|
|
50
|
+
h[meth] = stat.respond_to?(meth) ? stat.send(meth) : def_v(meth)
|
51
|
+
h
|
52
|
+
end
|
53
|
+
end # stat_hash(stat, syms
|
54
|
+
end # class::Stat < Openstruct
|
55
|
+
end # class::Archiverb
|
@@ -0,0 +1,214 @@
|
|
1
|
+
require 'archiverb'
|
2
|
+
require 'etc'
|
3
|
+
|
4
|
+
class Archiverb
|
5
|
+
# GNU tar implementation
|
6
|
+
# @see http://en.wikipedia.org/wiki/Tar_(file_format)
|
7
|
+
# @see http://www.gnu.org/software/tar/manual/html_node/Standard.html
|
8
|
+
# @see http://www.subspacefield.org/~vax/tar_format.html
|
9
|
+
class Tar < Archiverb
|
10
|
+
TMAGIC = 'ustar'
|
11
|
+
TVERSION = "00"
|
12
|
+
OLDGNU_MAGIC = "ustar \0"
|
13
|
+
REGTYPE = '0' # regular file
|
14
|
+
AREGTYPE = "\0" # regular file
|
15
|
+
LNKTYPE = '1' # link
|
16
|
+
SYMTYPE = '2' # reserved (symlink)
|
17
|
+
CHRTYPE = '3' # character special
|
18
|
+
BLKTYPE = '4' # block special
|
19
|
+
DIRTYPE = '5' # directory
|
20
|
+
FIFOTYPE = '6' # FIFO special
|
21
|
+
CONTTYPE = '7' # reserved (contiguous file)
|
22
|
+
XHDTYPE = 'x' # extended header referring to next file in archive
|
23
|
+
XGLTYPE = 'g' # global extended header
|
24
|
+
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def write_to(io)
|
29
|
+
@files.each do |name, file|
|
30
|
+
if name.length > 100
|
31
|
+
name, prefix = name[0..99], name[100..-1]
|
32
|
+
if prefix > 155
|
33
|
+
raise ArgumentError.new("file name cannot exceed 255 characters: #{name}#{prefix}")
|
34
|
+
end
|
35
|
+
else
|
36
|
+
prefix = ""
|
37
|
+
end
|
38
|
+
header = "#{name}" + ("\0" * (100 - name.length))
|
39
|
+
# @todo double check the modes on links
|
40
|
+
# input header: data/henryIV.txt0000755000076500000240000000000011706250470015332 2heneryIV.txtustar calebstaff
|
41
|
+
# output header: data/henryIV.txt0050423000076500000240000000001411706305252015333 2heneryIV.txtustar calebstaff
|
42
|
+
|
43
|
+
# offset 100
|
44
|
+
header += sprintf("%.7o\0", file.mode)
|
45
|
+
# offset 108
|
46
|
+
header += sprintf("%.7o\0", file.uid)
|
47
|
+
# offset 116
|
48
|
+
header += sprintf("%.7o\0", file.gid)
|
49
|
+
# offset 124
|
50
|
+
header += sprintf("%.11o\0", file.size)
|
51
|
+
# offset 136
|
52
|
+
header += sprintf("%.11o\0", file.mtime.to_i)
|
53
|
+
# offset 148
|
54
|
+
header += " " * 8 # write 8 blanks for the checksum, we'll replace it later
|
55
|
+
|
56
|
+
# offset 156
|
57
|
+
if [LNKTYPE, SYMTYPE].include?(type = tar_type(file.stat))
|
58
|
+
header += sprintf("%.1o", type)
|
59
|
+
raise ArgumentError.new("#{name} link type files' stat objects must contain a readlink") if file.stat.readlink.nil?
|
60
|
+
# offset 157
|
61
|
+
header += "#{file.stat.readlink}\0" + ("\0" * (99 - file.stat.readlink.length))
|
62
|
+
else
|
63
|
+
type = DIRTYPE if name[-1] == "/"
|
64
|
+
header += sprintf("%.1o", type)
|
65
|
+
# offset 157
|
66
|
+
header += "\0"*100
|
67
|
+
end
|
68
|
+
|
69
|
+
# offset 257
|
70
|
+
header += OLDGNU_MAGIC
|
71
|
+
|
72
|
+
uname = file.stat.uname || Etc.getpwuid(file.uid).name
|
73
|
+
gname = file.stat.gname || Etc.getgrgid(file.gid).name
|
74
|
+
# offset 265
|
75
|
+
header += "#{uname}\0" + ("\0" * (31 - uname.length))
|
76
|
+
# offset 297
|
77
|
+
header += "#{gname}\0" + ("\0" * (31 - gname.length))
|
78
|
+
|
79
|
+
# offset 329
|
80
|
+
if type == CHRTYPE || type ==BLKTYPE
|
81
|
+
header += sprintf("%.7o\0", file.stat.dev_major)
|
82
|
+
header += sprintf("%.7o\0", file.stat.dev_minor)
|
83
|
+
else
|
84
|
+
header += "\0" * 16
|
85
|
+
end
|
86
|
+
|
87
|
+
# offset 345
|
88
|
+
header += "#{prefix}" + ("\0" * (155 - (prefix || "").length))
|
89
|
+
|
90
|
+
# offset 500
|
91
|
+
header += "\0" * 12
|
92
|
+
|
93
|
+
header = header[0..147] + chksum(header) + header[156..-1]
|
94
|
+
io.write(header)
|
95
|
+
io.write(file.read).tap do |len|
|
96
|
+
unless (overflow = len % 512) == 0
|
97
|
+
io.write("\0" * (512 - (overflow)))
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end # name, file
|
101
|
+
|
102
|
+
io.write("\0" * 1024)
|
103
|
+
self
|
104
|
+
end # write_to(io)
|
105
|
+
|
106
|
+
def next_header(io)
|
107
|
+
return nil if io.eof?
|
108
|
+
if (raw = io.read(512)).strip == ""
|
109
|
+
return nil if(raw = io.read(512)).strip == ""
|
110
|
+
end # raw.strip == ""
|
111
|
+
|
112
|
+
header = {}
|
113
|
+
header[:name] = raw[0..99].strip
|
114
|
+
check = chksum(raw)
|
115
|
+
header[:chksum] = raw[148..155]
|
116
|
+
raise WrongChksum.new("#{header[:name]}: #{header[:chksum]} expected to be #{check}") unless header[:chksum] == check
|
117
|
+
header[:mode] = raw[100..107].to_i(8)
|
118
|
+
header[:uid] = Integer(raw[108..115].strip)
|
119
|
+
header[:gid] = Integer(raw[116..123].strip)
|
120
|
+
header[:size] = Integer(raw[124..135].strip)
|
121
|
+
header[:mtime] = raw[136..147].strip
|
122
|
+
header[:mtime] = "0#{header[:mtime]}" if header[:mtime].length == 11
|
123
|
+
header[:mtime] = Time.at(Integer(header[:mtime]))
|
124
|
+
# @todo check for XHDTYPE, or XGLTYPE
|
125
|
+
header[:ftype] = stat_type(raw[156])
|
126
|
+
header[:readlink] = raw[157..256].strip
|
127
|
+
header[:magic] = raw[257..262].strip
|
128
|
+
header[:version] = raw[263..264].strip
|
129
|
+
header[:uname] = raw[265..296].strip
|
130
|
+
header[:gname] = raw[297..328].strip
|
131
|
+
header[:dev_major] = raw[329..336].strip
|
132
|
+
header[:dev_minor] = raw[337..344].strip
|
133
|
+
header[:prefix] = raw[345..500].strip
|
134
|
+
header.merge!(pull_ustar(raw)) if header[:magic] == TMAGIC
|
135
|
+
header
|
136
|
+
end
|
137
|
+
|
138
|
+
def tar_type(stat)
|
139
|
+
case stat.ftype
|
140
|
+
when 'file'
|
141
|
+
REGTYPE
|
142
|
+
when 'directory'
|
143
|
+
DIRTYPE
|
144
|
+
when 'characterSpecial'
|
145
|
+
CHRTYPE
|
146
|
+
when 'blockSpecial'
|
147
|
+
BLKTYPE
|
148
|
+
when 'fifo'
|
149
|
+
FIFOTYPE
|
150
|
+
when 'link'
|
151
|
+
#LNKTYPE
|
152
|
+
SYMTYPE
|
153
|
+
else
|
154
|
+
warn "file type: #{stat.ftype} is not supported; treating it as a regular file"
|
155
|
+
REGTYPE
|
156
|
+
end
|
157
|
+
end # tar_type(stat)
|
158
|
+
|
159
|
+
def stat_type(bit)
|
160
|
+
case bit
|
161
|
+
when REGTYPE, AREGTYPE
|
162
|
+
'file'
|
163
|
+
when LNKTYPE, SYMTYPE
|
164
|
+
'link'
|
165
|
+
when CHRTYPE
|
166
|
+
'characterSpecial'
|
167
|
+
when BLKTYPE
|
168
|
+
'blockSpecial'
|
169
|
+
when DIRTYPE
|
170
|
+
'directory'
|
171
|
+
when FIFOTYPE
|
172
|
+
'fifo'
|
173
|
+
when CONTTYPE, XHDTYPE, XGLTYPE
|
174
|
+
'unknown'
|
175
|
+
else
|
176
|
+
warn "file type #{bit} is not supported; treating it as a regular file"
|
177
|
+
'regular'
|
178
|
+
end
|
179
|
+
end # stat_type(bit)
|
180
|
+
def read_file(header, io)
|
181
|
+
io.read(header[:size]).tap do |raw|
|
182
|
+
if (diff = header[:size] % 512) != 0
|
183
|
+
io.read(512 - diff)
|
184
|
+
end
|
185
|
+
end # raw
|
186
|
+
end
|
187
|
+
|
188
|
+
def pull_ustar(raw)
|
189
|
+
header = {}
|
190
|
+
header[:prefix] = raw[345].strip
|
191
|
+
header[:fill2] = raw[346].strip
|
192
|
+
header[:fill3] = raw[347..354].strip
|
193
|
+
header[:isextended] = raw[355]
|
194
|
+
header[:sparse] = raw[356..451].strip
|
195
|
+
header[:realsize] = raw[452..463].strip
|
196
|
+
header[:offset] = raw[464..475].strip
|
197
|
+
header[:atime] = raw[476..487].strip
|
198
|
+
header[:ctime] = raw[488..499].strip
|
199
|
+
header[:mfill] = raw[500..507].strip
|
200
|
+
header[:xmagic] = raw[508..511].strip
|
201
|
+
header
|
202
|
+
end # pull_ustar(header)
|
203
|
+
|
204
|
+
# The checksum is calculated by taking the sum of the unsigned byte
|
205
|
+
# values of the header block with the eight checksum bytes taken to
|
206
|
+
# be ascii spaces (decimal value 32). It is stored as a six digit
|
207
|
+
# octal number with leading zeroes followed by a NUL and then a
|
208
|
+
# space.
|
209
|
+
def chksum(header)
|
210
|
+
sprintf("%.6o\0 ", (header[0..147] + header[156..500]).each_byte.inject(256) { |s,b| s+b })
|
211
|
+
end
|
212
|
+
|
213
|
+
end # class::Tar < Archiverb
|
214
|
+
end # class::Archiverb
|
metadata
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: archiverb
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.9.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Caleb Crane
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-02-07 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rspec
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
description: ''
|
31
|
+
email:
|
32
|
+
- archiverb@simulacre.org
|
33
|
+
executables: []
|
34
|
+
extensions: []
|
35
|
+
extra_rdoc_files: []
|
36
|
+
files:
|
37
|
+
- lib/archiverb/ar.rb
|
38
|
+
- lib/archiverb/file.rb
|
39
|
+
- lib/archiverb/stat.rb
|
40
|
+
- lib/archiverb/tar.rb
|
41
|
+
- lib/archiverb/version.rb
|
42
|
+
- lib/archiverb.rb
|
43
|
+
- README.md
|
44
|
+
- LICENSE.txt
|
45
|
+
homepage: http://github.com/simulacre/archiverb
|
46
|
+
licenses:
|
47
|
+
- MIT
|
48
|
+
post_install_message:
|
49
|
+
rdoc_options: []
|
50
|
+
require_paths:
|
51
|
+
- lib
|
52
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
53
|
+
none: false
|
54
|
+
requirements:
|
55
|
+
- - ! '>='
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
version: '0'
|
58
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
59
|
+
none: false
|
60
|
+
requirements:
|
61
|
+
- - ! '>='
|
62
|
+
- !ruby/object:Gem::Version
|
63
|
+
version: 1.3.6
|
64
|
+
requirements: []
|
65
|
+
rubyforge_project:
|
66
|
+
rubygems_version: 1.8.24
|
67
|
+
signing_key:
|
68
|
+
specification_version: 3
|
69
|
+
summary: Native Ruby implementations of tar and ar archives
|
70
|
+
test_files: []
|
71
|
+
has_rdoc:
|