archiverb 0.9.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.
- 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:
|