omnizip 0.3.36 → 0.3.38
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 +4 -4
- data/CHANGELOG.md +135 -9
- data/docs/getting-started/basic-usage.adoc +14 -22
- data/docs/guides/advanced-features/encryption.adoc +16 -39
- data/docs/guides/advanced-features/index.adoc +16 -6
- data/docs/guides/advanced-features/parallel-processing.adoc +63 -165
- data/docs/guides/advanced-features/progress-tracking.adoc +65 -261
- data/docs/guides/advanced-features/streaming.adoc +43 -113
- data/docs/guides/archive-formats/gzip-format.adoc +2 -4
- data/docs/guides/archive-formats/ole-format.adoc +54 -219
- data/docs/guides/archive-formats/rar5.adoc +15 -15
- data/docs/guides/archive-formats/rpm-format.adoc +48 -189
- data/docs/guides/compression-algorithms/bzip2.adoc +2 -1
- data/docs/guides/compression-algorithms/deflate.adoc +1 -7
- data/docs/guides/compression-algorithms/lzma.adoc +10 -14
- data/docs/guides/compression-algorithms/lzma2.adoc +34 -117
- data/docs/guides/compression-algorithms/ppmd.adoc +24 -27
- data/docs/guides/compression-algorithms/zstandard.adoc +85 -150
- data/docs/reference/api/overview.adoc +16 -17
- data/docs/troubleshooting/index.adoc +8 -9
- data/lib/omnizip/algorithms/zstandard.rb +2 -3
- data/lib/omnizip/archive/builder.rb +48 -0
- data/lib/omnizip/archive/reader_session.rb +67 -0
- data/lib/omnizip/archive.rb +86 -0
- data/lib/omnizip/archive_handler.rb +3 -0
- data/lib/omnizip/archive_handlers/cpio_handler.rb +47 -0
- data/lib/omnizip/archive_handlers/iso_handler.rb +54 -0
- data/lib/omnizip/archive_handlers/rar_handler.rb +50 -0
- data/lib/omnizip/archive_handlers/seven_zip_handler.rb +6 -2
- data/lib/omnizip/archive_handlers/tar_handler.rb +4 -0
- data/lib/omnizip/archive_handlers/zip_handler.rb +25 -2
- data/lib/omnizip/archive_handlers.rb +3 -0
- data/lib/omnizip/cli.rb +25 -0
- data/lib/omnizip/convenience.rb +91 -11
- data/lib/omnizip/formats/iso/directory_builder.rb +24 -0
- data/lib/omnizip/formats/iso/reader.rb +2 -2
- data/lib/omnizip/formats/iso/writer.rb +20 -4
- data/lib/omnizip/formats/iso.rb +3 -1
- data/lib/omnizip/formats/rar/block_parser.rb +8 -12
- data/lib/omnizip/formats/rar/constants.rb +3 -1
- data/lib/omnizip/formats/rar/header.rb +16 -6
- data/lib/omnizip/formats/rar/rar5/compression/lzss.rb +5 -3
- data/lib/omnizip/formats/rar/rar5/header.rb +50 -33
- data/lib/omnizip/formats/rar/rar5/multi_volume/volume_writer.rb +17 -34
- data/lib/omnizip/formats/rar/rar5/vint.rb +25 -37
- data/lib/omnizip/formats/rar/rar5/writer.rb +50 -14
- data/lib/omnizip/formats/rar/reader.rb +6 -2
- data/lib/omnizip/formats/rar.rb +5 -3
- data/lib/omnizip/formats/seven_zip/parser.rb +22 -7
- data/lib/omnizip/formats/seven_zip/writer.rb +78 -19
- data/lib/omnizip/version.rb +1 -1
- data/lib/omnizip.rb +1 -0
- metadata +7 -1
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "tmpdir"
|
|
4
|
+
|
|
5
|
+
module Omnizip
|
|
6
|
+
module ArchiveHandlers
|
|
7
|
+
# Read-only adapter for the CPIO format (initramfs images, RPM
|
|
8
|
+
# payloads). Extraction and listing are supported; CPIO writing
|
|
9
|
+
# exists at the format level but is not exposed through the
|
|
10
|
+
# convenience archive API.
|
|
11
|
+
class CpioHandler
|
|
12
|
+
def create(_path, **_options)
|
|
13
|
+
raise Omnizip::UnsupportedFormatError,
|
|
14
|
+
"cpio archives cannot be created through the convenience " \
|
|
15
|
+
"API; use Omnizip::Formats::Cpio.create directly"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def extract_to(path, output_dir, **_)
|
|
19
|
+
Omnizip::Formats::Cpio.extract(path, output_dir)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def list(path, details: false, **_)
|
|
23
|
+
entries = Omnizip::Formats::Cpio.list(path)
|
|
24
|
+
if details
|
|
25
|
+
entries.map do |e|
|
|
26
|
+
{ name: e.name, size: e.data.bytesize,
|
|
27
|
+
directory: e.name.end_with?("/"), mtime: nil }
|
|
28
|
+
end
|
|
29
|
+
else
|
|
30
|
+
entries.map(&:name)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def read_entry(path, entry_name, **_)
|
|
35
|
+
entry = Omnizip::Formats::Cpio.list(path)
|
|
36
|
+
.find { |e| e.name == entry_name }
|
|
37
|
+
unless entry
|
|
38
|
+
raise Errno::ENOENT, "Entry not found: #{entry_name}"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
entry.data
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
Omnizip::ArchiveHandler.register(:cpio, Omnizip::ArchiveHandlers::CpioHandler.new)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "tmpdir"
|
|
4
|
+
|
|
5
|
+
module Omnizip
|
|
6
|
+
module ArchiveHandlers
|
|
7
|
+
# Read-only adapter for the ISO 9660 format (disk images).
|
|
8
|
+
# Extraction and listing are supported; ISO writing exists at the
|
|
9
|
+
# format level but is not exposed through the convenience archive
|
|
10
|
+
# API.
|
|
11
|
+
class IsoHandler
|
|
12
|
+
def create(_path, **_options)
|
|
13
|
+
raise Omnizip::UnsupportedFormatError,
|
|
14
|
+
"iso images cannot be created through the convenience " \
|
|
15
|
+
"API; use Omnizip::Formats::Iso.create directly"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def extract_to(path, output_dir, **_)
|
|
19
|
+
Omnizip::Formats::Iso.extract(path, output_dir)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def list(path, details: false, **_)
|
|
23
|
+
entries = Omnizip::Formats::Iso.list(path)
|
|
24
|
+
if details
|
|
25
|
+
entries.map do |e|
|
|
26
|
+
{ name: e.full_path, size: e.size, directory: e.directory?,
|
|
27
|
+
mtime: e.recording_date }
|
|
28
|
+
end
|
|
29
|
+
else
|
|
30
|
+
entries.map(&:full_path)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def read_entry(path, entry_name, **_)
|
|
35
|
+
entry = Omnizip::Formats::Iso.list(path)
|
|
36
|
+
.find { |e| e.full_path == entry_name }
|
|
37
|
+
unless entry
|
|
38
|
+
raise Errno::ENOENT, "Entry not found: #{entry_name}"
|
|
39
|
+
end
|
|
40
|
+
raise Errno::EISDIR, "Entry is a directory: #{entry_name}" if entry.directory?
|
|
41
|
+
|
|
42
|
+
Dir.mktmpdir("omnizip-iso-entry") do |dir|
|
|
43
|
+
dest = File.join(dir, "entry")
|
|
44
|
+
Omnizip::Formats::Iso.open(path) do |iso|
|
|
45
|
+
iso.extract_entry(entry_name, dest)
|
|
46
|
+
end
|
|
47
|
+
File.binread(dest)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
Omnizip::ArchiveHandler.register(:iso, Omnizip::ArchiveHandlers::IsoHandler.new)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "tmpdir"
|
|
4
|
+
|
|
5
|
+
module Omnizip
|
|
6
|
+
module ArchiveHandlers
|
|
7
|
+
# Read-only adapter for the RAR format. RAR compression is
|
|
8
|
+
# patented software (see link:README[RAR write support]); this
|
|
9
|
+
# handler exposes extraction/listing/reading only, so the
|
|
10
|
+
# convenience API and the Archive facade can operate on existing
|
|
11
|
+
# archives truthfully instead of raising on every operation.
|
|
12
|
+
class RarHandler
|
|
13
|
+
def create(_path, **_options)
|
|
14
|
+
raise Omnizip::UnsupportedFormatError,
|
|
15
|
+
"RAR archives cannot be created (patented format); " \
|
|
16
|
+
"Omnizip reads RAR archives only"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def extract_to(path, output_dir, password: nil, **_)
|
|
20
|
+
Omnizip::Formats::Rar::Decompressor.extract(path, output_dir,
|
|
21
|
+
password: password)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def list(path, details: false, **_)
|
|
25
|
+
reader = Omnizip::Formats::Rar::Reader.new(path).open
|
|
26
|
+
entries = reader.list_files
|
|
27
|
+
if details
|
|
28
|
+
entries.map do |e|
|
|
29
|
+
{ name: e.name, size: e.size, directory: e.is_dir,
|
|
30
|
+
mtime: e.mtime }
|
|
31
|
+
end
|
|
32
|
+
else
|
|
33
|
+
entries.map(&:name)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def read_entry(path, entry_name, password: nil, **_)
|
|
38
|
+
Dir.mktmpdir("omnizip-rar-entry") do |dir|
|
|
39
|
+
dest = File.join(dir, "entry")
|
|
40
|
+
Omnizip::Formats::Rar::Decompressor.extract_entry(
|
|
41
|
+
path, entry_name, dest, password: password
|
|
42
|
+
)
|
|
43
|
+
File.binread(dest)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
Omnizip::ArchiveHandler.register(:rar, Omnizip::ArchiveHandlers::RarHandler.new)
|
|
@@ -8,8 +8,8 @@ module Omnizip
|
|
|
8
8
|
# Creates a .7z archive. The yielded proxy only supports
|
|
9
9
|
# +add(name, source_path)+ for files — directory entries are not
|
|
10
10
|
# part of the single-file convenience flow.
|
|
11
|
-
def create(path, &block)
|
|
12
|
-
Omnizip::Formats::SevenZip.create(path) do |writer|
|
|
11
|
+
def create(path, **options, &block)
|
|
12
|
+
Omnizip::Formats::SevenZip.create(path, options) do |writer|
|
|
13
13
|
block&.call(FileProxy.new(writer))
|
|
14
14
|
end
|
|
15
15
|
end
|
|
@@ -73,6 +73,10 @@ module Omnizip
|
|
|
73
73
|
|
|
74
74
|
@writer.add_file(source_path, name)
|
|
75
75
|
end
|
|
76
|
+
|
|
77
|
+
def add_data(name, data)
|
|
78
|
+
@writer.add_data(name, data)
|
|
79
|
+
end
|
|
76
80
|
end
|
|
77
81
|
end
|
|
78
82
|
end
|
|
@@ -5,8 +5,13 @@ module Omnizip
|
|
|
5
5
|
# Adapter that exposes the canonical +create+/+open+/+extract_to+
|
|
6
6
|
# /+list+ interface for the ZIP format. Wraps +Omnizip::Zip::File+.
|
|
7
7
|
class ZipHandler
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
# Zip::File has no archive-level compression options (method and
|
|
9
|
+
# level are per-entry in Zip::OutputStream), so extra keywords —
|
|
10
|
+
# e.g. profile-injected ones — are absorbed here.
|
|
11
|
+
def create(path, **_options, &block)
|
|
12
|
+
Omnizip::Zip::File.create(path) do |file|
|
|
13
|
+
block&.call(FileProxy.new(file))
|
|
14
|
+
end
|
|
10
15
|
end
|
|
11
16
|
|
|
12
17
|
def open(path, &block)
|
|
@@ -66,6 +71,24 @@ module Omnizip
|
|
|
66
71
|
def remove_entry(path, entry_name)
|
|
67
72
|
Omnizip::Zip::File.open(path) { |zip| zip.remove(entry_name) }
|
|
68
73
|
end
|
|
74
|
+
|
|
75
|
+
# Translates the generic +add(name, source_path)+ and
|
|
76
|
+
# +add_data(name, data)+ interface. +Zip::File#add_data+ is
|
|
77
|
+
# private, so in-memory content goes through the public block
|
|
78
|
+
# form +add(name) { data }+.
|
|
79
|
+
class FileProxy
|
|
80
|
+
def initialize(file)
|
|
81
|
+
@file = file
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def add(name, source_path = nil)
|
|
85
|
+
@file.add(name, source_path)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def add_data(name, data)
|
|
89
|
+
@file.add(name) { data }
|
|
90
|
+
end
|
|
91
|
+
end
|
|
69
92
|
end
|
|
70
93
|
end
|
|
71
94
|
end
|
|
@@ -8,5 +8,8 @@ module Omnizip
|
|
|
8
8
|
autoload :ZipHandler, "omnizip/archive_handlers/zip_handler"
|
|
9
9
|
autoload :TarHandler, "omnizip/archive_handlers/tar_handler"
|
|
10
10
|
autoload :SevenZipHandler, "omnizip/archive_handlers/seven_zip_handler"
|
|
11
|
+
autoload :RarHandler, "omnizip/archive_handlers/rar_handler"
|
|
12
|
+
autoload :CpioHandler, "omnizip/archive_handlers/cpio_handler"
|
|
13
|
+
autoload :IsoHandler, "omnizip/archive_handlers/iso_handler"
|
|
11
14
|
end
|
|
12
15
|
end
|
data/lib/omnizip/cli.rb
CHANGED
|
@@ -25,6 +25,28 @@ module Omnizip
|
|
|
25
25
|
# under Omnizip::Cli would re-trigger this file's autoload
|
|
26
26
|
# mid-load.
|
|
27
27
|
module Shared
|
|
28
|
+
# Thor 1.x does not treat --help/-h on subcommands as a request
|
|
29
|
+
# for help: the flag reaches the command as a positional argument
|
|
30
|
+
# and arity checking fails. Classes extending this module route
|
|
31
|
+
# those flags to the standard help output instead.
|
|
32
|
+
module HelpDispatch
|
|
33
|
+
def dispatch(meth, given_args, given_opts, config)
|
|
34
|
+
wants_help = [Array(given_args), Array(given_opts)].any? do |list|
|
|
35
|
+
list.include?("--help") || list.include?("-h")
|
|
36
|
+
end
|
|
37
|
+
if wants_help
|
|
38
|
+
name = meth || Array(given_args).grep_v(/\A-/).first
|
|
39
|
+
# Subcommand groups intercept their own --help (e.g.
|
|
40
|
+
# "archive create --help" must show the create usage).
|
|
41
|
+
return super if subcommands.include?(name)
|
|
42
|
+
|
|
43
|
+
return start(["help", name].compact, config)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
super
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
28
50
|
# Print a formatted error message and exit nonzero.
|
|
29
51
|
#
|
|
30
52
|
# @param error [StandardError] the error to display
|
|
@@ -51,6 +73,7 @@ module Omnizip
|
|
|
51
73
|
# Profile commands subcommand group
|
|
52
74
|
class ProfileCommands < Thor
|
|
53
75
|
include Shared
|
|
76
|
+
extend Shared::HelpDispatch
|
|
54
77
|
|
|
55
78
|
class << self
|
|
56
79
|
def exit_on_failure?
|
|
@@ -99,6 +122,7 @@ module Omnizip
|
|
|
99
122
|
# Archive commands subcommand group
|
|
100
123
|
class ArchiveCommands < Thor
|
|
101
124
|
include Shared
|
|
125
|
+
extend Shared::HelpDispatch
|
|
102
126
|
|
|
103
127
|
class << self
|
|
104
128
|
def exit_on_failure?
|
|
@@ -328,6 +352,7 @@ module Omnizip
|
|
|
328
352
|
# files using various compression algorithms.
|
|
329
353
|
class Cli < Thor
|
|
330
354
|
include Shared
|
|
355
|
+
extend Shared::HelpDispatch
|
|
331
356
|
|
|
332
357
|
class << self
|
|
333
358
|
def exit_on_failure?
|
data/lib/omnizip/convenience.rb
CHANGED
|
@@ -40,13 +40,50 @@ module Omnizip
|
|
|
40
40
|
return output_path
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
-
Omnizip::ArchiveHandler.for(resolve_archive_format(output_path, format)).create(output_path) do |archive|
|
|
43
|
+
Omnizip::ArchiveHandler.for(resolve_archive_format(output_path, format)).create(output_path, **options) do |archive|
|
|
44
44
|
archive.add(::File.basename(input_path), input_path)
|
|
45
45
|
end
|
|
46
46
|
|
|
47
47
|
output_path
|
|
48
48
|
end
|
|
49
49
|
|
|
50
|
+
# Decompress a single-file compressed stream (the counterpart of
|
|
51
|
+
# #compress_file). The format is resolved from the input
|
|
52
|
+
# extension: .gz, .bz2, .xz, .lzma, .lz.
|
|
53
|
+
#
|
|
54
|
+
# @param compressed_path [String] Path to the compressed file
|
|
55
|
+
# @param output_path [String] Path to write the decompressed data
|
|
56
|
+
# @return [String] +output_path+
|
|
57
|
+
def decompress_file(compressed_path, output_path)
|
|
58
|
+
require_archive!(compressed_path)
|
|
59
|
+
|
|
60
|
+
format_class = DECOMPRESSORS[::File.extname(compressed_path).downcase]
|
|
61
|
+
unless format_class
|
|
62
|
+
raise Omnizip::UnsupportedFormatError,
|
|
63
|
+
"decompress_file supports #{DECOMPRESSORS.keys.join(', ')}; " \
|
|
64
|
+
"use extract_archive for archive formats"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
case format_class
|
|
68
|
+
when :xz
|
|
69
|
+
::File.binwrite(output_path, Formats::Xz.decompress(compressed_path))
|
|
70
|
+
when :zstandard
|
|
71
|
+
zstd = Algorithms::Zstandard.new
|
|
72
|
+
::File.open(compressed_path, "rb") do |input_io|
|
|
73
|
+
::File.open(output_path, "wb") do |output_io|
|
|
74
|
+
zstd.decompress(input_io, output_io)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
else
|
|
78
|
+
::File.open(compressed_path, "rb") do |input_io|
|
|
79
|
+
::File.open(output_path, "wb") do |output_io|
|
|
80
|
+
format_class.decompress_stream(input_io, output_io)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
output_path
|
|
85
|
+
end
|
|
86
|
+
|
|
50
87
|
# Compress a directory into an archive.
|
|
51
88
|
#
|
|
52
89
|
# @param input_dir [String] Path to input directory
|
|
@@ -64,12 +101,23 @@ module Omnizip
|
|
|
64
101
|
raise ArgumentError, "Input is not a directory: #{input_dir}"
|
|
65
102
|
end
|
|
66
103
|
|
|
104
|
+
# A directory cannot be written through a single-file stream
|
|
105
|
+
# format; routing it to the default would silently produce a
|
|
106
|
+
# ZIP under a foreign extension.
|
|
107
|
+
if format == DEFAULT_FORMAT && single_file_handler(output_path)
|
|
108
|
+
raise Omnizip::UnsupportedFormatError,
|
|
109
|
+
"#{::File.extname(output_path).downcase} is a single-file " \
|
|
110
|
+
"stream format and cannot contain a directory; use an " \
|
|
111
|
+
"archive format (.zip/.tar/.7z) or compress entries " \
|
|
112
|
+
"individually"
|
|
113
|
+
end
|
|
114
|
+
|
|
67
115
|
if options[:profile]
|
|
68
116
|
first_file = find_first_file(input_dir)
|
|
69
117
|
apply_profile(first_file, options)
|
|
70
118
|
end
|
|
71
119
|
|
|
72
|
-
Omnizip::ArchiveHandler.for(resolve_archive_format(output_path, format)).create(output_path) do |archive|
|
|
120
|
+
Omnizip::ArchiveHandler.for(resolve_archive_format(output_path, format)).create(output_path, **options) do |archive|
|
|
73
121
|
add_directory_contents(archive, input_dir, "", recursive: recursive)
|
|
74
122
|
end
|
|
75
123
|
|
|
@@ -87,7 +135,7 @@ module Omnizip
|
|
|
87
135
|
def extract_archive(archive_path, output_dir, format: DEFAULT_FORMAT,
|
|
88
136
|
overwrite: false, **options)
|
|
89
137
|
require_archive!(archive_path)
|
|
90
|
-
Omnizip::ArchiveHandler.for(resolve_archive_format(archive_path, format))
|
|
138
|
+
Omnizip::ArchiveHandler.for(resolve_archive_format(archive_path, format, writing: false))
|
|
91
139
|
.extract_to(archive_path, output_dir,
|
|
92
140
|
overwrite: overwrite, **options)
|
|
93
141
|
end
|
|
@@ -102,7 +150,7 @@ module Omnizip
|
|
|
102
150
|
def list_archive(archive_path, format: DEFAULT_FORMAT, details: false,
|
|
103
151
|
**options)
|
|
104
152
|
require_archive!(archive_path)
|
|
105
|
-
Omnizip::ArchiveHandler.for(resolve_archive_format(archive_path, format))
|
|
153
|
+
Omnizip::ArchiveHandler.for(resolve_archive_format(archive_path, format, writing: false))
|
|
106
154
|
.list(archive_path, details: details, **options)
|
|
107
155
|
end
|
|
108
156
|
|
|
@@ -114,7 +162,7 @@ module Omnizip
|
|
|
114
162
|
# @return [String] Entry contents
|
|
115
163
|
def read_from_archive(archive_path, entry_name, format: DEFAULT_FORMAT)
|
|
116
164
|
require_archive!(archive_path)
|
|
117
|
-
Omnizip::ArchiveHandler.for(resolve_archive_format(archive_path, format)).read_entry(archive_path, entry_name)
|
|
165
|
+
Omnizip::ArchiveHandler.for(resolve_archive_format(archive_path, format, writing: false)).read_entry(archive_path, entry_name)
|
|
118
166
|
end
|
|
119
167
|
|
|
120
168
|
# Add a file to an existing archive.
|
|
@@ -182,12 +230,34 @@ module Omnizip
|
|
|
182
230
|
".7z" => :seven_zip,
|
|
183
231
|
}.freeze
|
|
184
232
|
|
|
185
|
-
# Extensions naming
|
|
186
|
-
#
|
|
187
|
-
#
|
|
188
|
-
#
|
|
233
|
+
# Extensions naming formats with read-only convenience routing.
|
|
234
|
+
# Writing them a ZIP under a foreign name was silent corruption;
|
|
235
|
+
# creation raises truthfully instead (the format-level writers
|
|
236
|
+
# exist for RAR/CPIO/ISO but are not part of the archive API).
|
|
189
237
|
READ_ONLY_FORMAT_EXTENSIONS = [".rar", ".iso", ".cpio"].freeze
|
|
190
238
|
|
|
239
|
+
# Extensions whose format has a READ-ONLY handler: extraction and
|
|
240
|
+
# listing route to it, while creation keeps raising.
|
|
241
|
+
READ_ARCHIVE_FORMAT_EXTENSIONS = {
|
|
242
|
+
".rar" => :rar,
|
|
243
|
+
".cpio" => :cpio,
|
|
244
|
+
".iso" => :iso,
|
|
245
|
+
}.freeze
|
|
246
|
+
|
|
247
|
+
# Extension -> single-file decompressor (stream interface).
|
|
248
|
+
# The Gzip/Bzip2File/Xz classes take path or stream; use the
|
|
249
|
+
# ones exposing decompress_stream for symmetry.
|
|
250
|
+
# Xz exposes a path-based decompress rather than a stream one,
|
|
251
|
+
# so it is marked specially here.
|
|
252
|
+
DECOMPRESSORS = {
|
|
253
|
+
".gz" => Formats::Gzip,
|
|
254
|
+
".bz2" => Formats::Bzip2File,
|
|
255
|
+
".xz" => :xz,
|
|
256
|
+
".lzma" => Formats::LzmaAlone,
|
|
257
|
+
".lz" => Formats::Lzip,
|
|
258
|
+
".zst" => :zstandard,
|
|
259
|
+
}.freeze
|
|
260
|
+
|
|
191
261
|
SINGLE_FILE_COMPRESSORS = {
|
|
192
262
|
".gz" => lambda do |input, output, options|
|
|
193
263
|
Omnizip::Formats::Gzip.compress(input, output, options)
|
|
@@ -214,6 +284,14 @@ module Omnizip
|
|
|
214
284
|
end
|
|
215
285
|
end
|
|
216
286
|
end,
|
|
287
|
+
".zst" => lambda do |input, output, options|
|
|
288
|
+
::File.open(input, "rb") do |input_io|
|
|
289
|
+
::File.open(output, "wb") do |output_io|
|
|
290
|
+
Omnizip::Algorithms::Zstandard.new.compress(input_io, output_io,
|
|
291
|
+
options)
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
end,
|
|
217
295
|
}.freeze
|
|
218
296
|
|
|
219
297
|
# The single-file compressor matching the output extension, or nil.
|
|
@@ -225,12 +303,14 @@ module Omnizip
|
|
|
225
303
|
# Archive format for a path: an explicit +format+ wins unless it
|
|
226
304
|
# is the default; otherwise the extension routes. Known-but-
|
|
227
305
|
# unwritable extensions raise instead of receiving a mislabeled
|
|
228
|
-
# ZIP.
|
|
229
|
-
|
|
306
|
+
# ZIP. +writing:+ false (read operations) additionally routes
|
|
307
|
+
# extensions with read-only handlers.
|
|
308
|
+
def resolve_archive_format(path, format, writing: true)
|
|
230
309
|
return format unless format == DEFAULT_FORMAT
|
|
231
310
|
|
|
232
311
|
ext = ::File.extname(path).downcase
|
|
233
312
|
routed = ARCHIVE_FORMAT_EXTENSIONS[ext]
|
|
313
|
+
routed ||= READ_ARCHIVE_FORMAT_EXTENSIONS[ext] unless writing
|
|
234
314
|
return routed if routed
|
|
235
315
|
|
|
236
316
|
if READ_ONLY_FORMAT_EXTENSIONS.include?(ext)
|
|
@@ -43,6 +43,12 @@ module Omnizip
|
|
|
43
43
|
# Allocate sectors for directories and files
|
|
44
44
|
allocate_sectors(tree)
|
|
45
45
|
|
|
46
|
+
# Tree nodes are separate hashes from @files entries; copy
|
|
47
|
+
# the allocated file locations onto the tree so directory
|
|
48
|
+
# records reference the real extents (otherwise every
|
|
49
|
+
# record points at sector 0).
|
|
50
|
+
assign_file_locations(tree)
|
|
51
|
+
|
|
46
52
|
# Build path table
|
|
47
53
|
path_table = build_path_table(tree)
|
|
48
54
|
|
|
@@ -143,6 +149,24 @@ module Omnizip
|
|
|
143
149
|
allocate_file_sectors
|
|
144
150
|
end
|
|
145
151
|
|
|
152
|
+
# Copy allocated file locations onto the matching tree nodes
|
|
153
|
+
#
|
|
154
|
+
# @param tree [Hash] Directory tree root
|
|
155
|
+
def assign_file_locations(tree)
|
|
156
|
+
walk = lambda do |node|
|
|
157
|
+
(node[:children] || []).each do |child|
|
|
158
|
+
if child[:directory]
|
|
159
|
+
walk.call(child)
|
|
160
|
+
else
|
|
161
|
+
match = @files.find { |f| f[:iso_path] == child[:iso_path] }
|
|
162
|
+
child[:location] = match[:location] if match
|
|
163
|
+
child[:size] = match[:size] if match
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
walk.call(tree)
|
|
168
|
+
end
|
|
169
|
+
|
|
146
170
|
# Allocate sectors for a directory
|
|
147
171
|
#
|
|
148
172
|
# @param dir_node [Hash] Directory node
|
|
@@ -90,8 +90,8 @@ module Omnizip
|
|
|
90
90
|
@entries.each do |entry|
|
|
91
91
|
next if entry.current_directory? || entry.parent_directory?
|
|
92
92
|
|
|
93
|
-
output_path = File.join(output_dir, entry.
|
|
94
|
-
extract_entry(entry.
|
|
93
|
+
output_path = File.join(output_dir, entry.full_path)
|
|
94
|
+
extract_entry(entry.full_path, output_path)
|
|
95
95
|
end
|
|
96
96
|
end
|
|
97
97
|
|
|
@@ -178,8 +178,13 @@ module Omnizip
|
|
|
178
178
|
preparer: "OMNIZIP #{Omnizip::VERSION}",
|
|
179
179
|
application: "OMNIZIP",
|
|
180
180
|
level: 2,
|
|
181
|
-
|
|
182
|
-
|
|
181
|
+
# Rock Ridge System Use fields and Joliet UCS-2 directory
|
|
182
|
+
# trees are not implemented; advertising either extension
|
|
183
|
+
# makes readers misparse the image (e.g. 7-Zip reads the
|
|
184
|
+
# ASCII names as UCS-2 via the cloned SVD), so both stay
|
|
185
|
+
# off until real implementations land.
|
|
186
|
+
rock_ridge: false,
|
|
187
|
+
joliet: false,
|
|
183
188
|
}
|
|
184
189
|
end
|
|
185
190
|
|
|
@@ -193,6 +198,7 @@ module Omnizip
|
|
|
193
198
|
source: File.expand_path(dir_path),
|
|
194
199
|
iso_path: iso_path,
|
|
195
200
|
stat: File.stat(dir_path),
|
|
201
|
+
directory: true,
|
|
196
202
|
}
|
|
197
203
|
|
|
198
204
|
# Add all contents
|
|
@@ -219,6 +225,7 @@ module Omnizip
|
|
|
219
225
|
source: File.expand_path(dir_path),
|
|
220
226
|
iso_path: iso_path,
|
|
221
227
|
stat: File.stat(dir_path),
|
|
228
|
+
directory: true,
|
|
222
229
|
}
|
|
223
230
|
|
|
224
231
|
Dir.foreach(dir_path) do |entry|
|
|
@@ -283,13 +290,22 @@ module Omnizip
|
|
|
283
290
|
# @param builder [VolumeBuilder] Volume builder
|
|
284
291
|
# @param dir_structure [Hash] Directory structure
|
|
285
292
|
def write_volume_descriptors(io, builder, dir_structure)
|
|
293
|
+
# Fixed layout: 16 PVD, 17 terminator, 18 path table (L),
|
|
294
|
+
# 19 path table (BE), directory data from sector 22.
|
|
295
|
+
root = dir_structure[:root].merge(
|
|
296
|
+
total_sectors: dir_structure[:total_sectors],
|
|
297
|
+
path_table_size: dir_structure[:path_table_size],
|
|
298
|
+
path_table_location: 18,
|
|
299
|
+
path_table_location_be: 19,
|
|
300
|
+
)
|
|
301
|
+
|
|
286
302
|
# Write primary volume descriptor
|
|
287
|
-
pvd = builder.build_primary(
|
|
303
|
+
pvd = builder.build_primary(root)
|
|
288
304
|
io.write(pvd)
|
|
289
305
|
|
|
290
306
|
# Write Joliet supplementary descriptor if enabled
|
|
291
307
|
if @joliet
|
|
292
|
-
svd = builder.build_joliet(
|
|
308
|
+
svd = builder.build_joliet(root)
|
|
293
309
|
io.write(svd)
|
|
294
310
|
end
|
|
295
311
|
|
data/lib/omnizip/formats/iso.rb
CHANGED
|
@@ -69,7 +69,9 @@ module Omnizip
|
|
|
69
69
|
# @param path [String] Path to ISO file
|
|
70
70
|
# @return [Array<DirectoryRecord>] Directory entries
|
|
71
71
|
def self.list(path)
|
|
72
|
-
|
|
72
|
+
entries = nil
|
|
73
|
+
open(path) { |iso| entries = iso.entries }
|
|
74
|
+
entries
|
|
73
75
|
end
|
|
74
76
|
|
|
75
77
|
# Extract ISO contents
|
|
@@ -121,6 +121,10 @@ module Omnizip
|
|
|
121
121
|
|
|
122
122
|
header_flags = read_vint(io)
|
|
123
123
|
|
|
124
|
+
# Optional ExtraAreaSize / DataSize precede the file fields
|
|
125
|
+
extra_size = header_flags.anybits?(RAR5_FLAG_EXTRA_AREA) ? read_vint(io) : 0
|
|
126
|
+
data_size = header_flags.anybits?(RAR5_FLAG_DATA_AREA) ? read_vint(io) : 0
|
|
127
|
+
|
|
124
128
|
# Read file header
|
|
125
129
|
file_flags = read_vint(io)
|
|
126
130
|
unpack_size = read_vint(io)
|
|
@@ -145,7 +149,7 @@ module Omnizip
|
|
|
145
149
|
|
|
146
150
|
# Set entry properties
|
|
147
151
|
entry.size = unpack_size
|
|
148
|
-
entry.compressed_size =
|
|
152
|
+
entry.compressed_size = data_size
|
|
149
153
|
entry.crc = crc
|
|
150
154
|
entry.host_os = host_os
|
|
151
155
|
entry.flags = file_flags
|
|
@@ -154,17 +158,9 @@ module Omnizip
|
|
|
154
158
|
entry.is_dir = file_flags.anybits?(RAR5_FLAG_IS_DIR)
|
|
155
159
|
entry.version = 5
|
|
156
160
|
|
|
157
|
-
#
|
|
158
|
-
if
|
|
159
|
-
|
|
160
|
-
io.read(extra_size) if extra_size.positive?
|
|
161
|
-
end
|
|
162
|
-
|
|
163
|
-
# Read data area if present
|
|
164
|
-
if header_flags.anybits?(RAR5_FLAG_DATA_AREA)
|
|
165
|
-
data_size = read_vint(io)
|
|
166
|
-
io.read(data_size) if data_size.positive?
|
|
167
|
-
end
|
|
161
|
+
# Skip extra area, then compressed data
|
|
162
|
+
io.read(extra_size) if extra_size.positive?
|
|
163
|
+
io.read(data_size) if data_size.positive?
|
|
168
164
|
|
|
169
165
|
entry
|
|
170
166
|
end
|
|
@@ -58,7 +58,9 @@ module Omnizip
|
|
|
58
58
|
RAR5_FLAG_UNKNOWN_BLOCKS = 0x0004
|
|
59
59
|
RAR5_FLAG_DATA_INHERITED = 0x0008
|
|
60
60
|
RAR5_FLAG_CHILD_BLOCKS = 0x0010
|
|
61
|
-
|
|
61
|
+
# File-header-specific flags (FileFlags field): the directory
|
|
62
|
+
# bit is 0x0001 per the RAR5 spec.
|
|
63
|
+
RAR5_FLAG_IS_DIR = 0x0001
|
|
62
64
|
RAR5_FLAG_MULTI_VOLUME = 0x0001
|
|
63
65
|
|
|
64
66
|
# Compression methods
|
|
@@ -122,9 +122,11 @@ module Omnizip
|
|
|
122
122
|
#
|
|
123
123
|
# @param io [IO] Input stream
|
|
124
124
|
def parse_rar5_header(io)
|
|
125
|
-
# RAR5 uses variable-length integer encoding
|
|
125
|
+
# RAR5 uses variable-length integer encoding. Layout per the
|
|
126
|
+
# RAR 5.0 format: HeaderCRC32(4), Header size(vint), Header
|
|
127
|
+
# type(vint), Header flags(vint), then type-specific fields.
|
|
126
128
|
read_uint32(io)
|
|
127
|
-
read_vint(io)
|
|
129
|
+
read_vint(io) # header size (redundant: areas carry their own)
|
|
128
130
|
header_type = read_vint(io)
|
|
129
131
|
header_flags = read_vint(io)
|
|
130
132
|
|
|
@@ -133,12 +135,20 @@ module Omnizip
|
|
|
133
135
|
end
|
|
134
136
|
|
|
135
137
|
@flags = header_flags
|
|
136
|
-
@is_multi_volume = header_flags.anybits?(RAR5_FLAG_MULTI_VOLUME)
|
|
137
138
|
|
|
138
|
-
|
|
139
|
-
|
|
139
|
+
extra_size = 0
|
|
140
|
+
if header_flags.anybits?(RAR5_FLAG_EXTRA_AREA)
|
|
141
|
+
extra_size = read_vint(io)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Archive flags: 0x0001 volume, 0x0002 volume number
|
|
145
|
+
# present, 0x0004 solid, 0x0008 recovery, 0x0010 locked
|
|
146
|
+
archive_flags = read_vint(io)
|
|
147
|
+
@is_multi_volume = archive_flags.anybits?(0x0001)
|
|
148
|
+
@is_solid = archive_flags.anybits?(0x0004)
|
|
149
|
+
@is_locked = archive_flags.anybits?(0x0010)
|
|
150
|
+
read_vint(io) if archive_flags.anybits?(0x0002) # volume number
|
|
140
151
|
|
|
141
|
-
extra_size = read_vint(io)
|
|
142
152
|
io.read(extra_size) if extra_size.positive?
|
|
143
153
|
end
|
|
144
154
|
|