cabriolet 0.1.2 → 0.2.1
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/README.adoc +703 -38
- data/lib/cabriolet/algorithm_factory.rb +250 -0
- data/lib/cabriolet/base_compressor.rb +206 -0
- data/lib/cabriolet/binary/bitstream.rb +167 -16
- data/lib/cabriolet/binary/bitstream_writer.rb +150 -21
- data/lib/cabriolet/binary/chm_structures.rb +2 -2
- data/lib/cabriolet/binary/hlp_structures.rb +258 -37
- data/lib/cabriolet/binary/lit_structures.rb +231 -65
- data/lib/cabriolet/binary/oab_structures.rb +17 -1
- data/lib/cabriolet/cab/command_handler.rb +226 -0
- data/lib/cabriolet/cab/compressor.rb +108 -84
- data/lib/cabriolet/cab/decompressor.rb +16 -20
- data/lib/cabriolet/cab/extractor.rb +142 -66
- data/lib/cabriolet/cab/file_compression_work.rb +52 -0
- data/lib/cabriolet/cab/file_compression_worker.rb +89 -0
- data/lib/cabriolet/checksum.rb +49 -0
- data/lib/cabriolet/chm/command_handler.rb +227 -0
- data/lib/cabriolet/chm/compressor.rb +7 -3
- data/lib/cabriolet/chm/decompressor.rb +39 -21
- data/lib/cabriolet/chm/parser.rb +5 -2
- data/lib/cabriolet/cli/base_command_handler.rb +127 -0
- data/lib/cabriolet/cli/command_dispatcher.rb +140 -0
- data/lib/cabriolet/cli/command_registry.rb +83 -0
- data/lib/cabriolet/cli.rb +356 -607
- data/lib/cabriolet/collections/file_collection.rb +175 -0
- data/lib/cabriolet/compressors/base.rb +1 -1
- data/lib/cabriolet/compressors/lzx.rb +241 -54
- data/lib/cabriolet/compressors/mszip.rb +35 -3
- data/lib/cabriolet/compressors/quantum.rb +36 -95
- data/lib/cabriolet/decompressors/base.rb +1 -1
- data/lib/cabriolet/decompressors/lzss.rb +13 -3
- data/lib/cabriolet/decompressors/lzx.rb +70 -33
- data/lib/cabriolet/decompressors/mszip.rb +126 -39
- data/lib/cabriolet/decompressors/quantum.rb +83 -53
- data/lib/cabriolet/errors.rb +3 -0
- data/lib/cabriolet/extraction/base_extractor.rb +88 -0
- data/lib/cabriolet/extraction/extractor.rb +171 -0
- data/lib/cabriolet/extraction/file_extraction_work.rb +60 -0
- data/lib/cabriolet/extraction/file_extraction_worker.rb +106 -0
- data/lib/cabriolet/file_entry.rb +156 -0
- data/lib/cabriolet/file_manager.rb +144 -0
- data/lib/cabriolet/format_base.rb +79 -0
- data/lib/cabriolet/hlp/command_handler.rb +282 -0
- data/lib/cabriolet/hlp/compressor.rb +28 -238
- data/lib/cabriolet/hlp/decompressor.rb +107 -147
- data/lib/cabriolet/hlp/parser.rb +52 -101
- data/lib/cabriolet/hlp/quickhelp/compression_stream.rb +138 -0
- data/lib/cabriolet/hlp/quickhelp/compressor.rb +151 -0
- data/lib/cabriolet/hlp/quickhelp/decompressor.rb +558 -0
- data/lib/cabriolet/hlp/quickhelp/file_writer.rb +125 -0
- data/lib/cabriolet/hlp/quickhelp/huffman_stream.rb +74 -0
- data/lib/cabriolet/hlp/quickhelp/huffman_tree.rb +167 -0
- data/lib/cabriolet/hlp/quickhelp/offset_calculator.rb +61 -0
- data/lib/cabriolet/hlp/quickhelp/parser.rb +274 -0
- data/lib/cabriolet/hlp/quickhelp/structure_builder.rb +93 -0
- data/lib/cabriolet/hlp/quickhelp/topic_builder.rb +52 -0
- data/lib/cabriolet/hlp/quickhelp/topic_compressor.rb +83 -0
- data/lib/cabriolet/hlp/winhelp/btree_builder.rb +289 -0
- data/lib/cabriolet/hlp/winhelp/compressor.rb +400 -0
- data/lib/cabriolet/hlp/winhelp/decompressor.rb +192 -0
- data/lib/cabriolet/hlp/winhelp/parser.rb +484 -0
- data/lib/cabriolet/hlp/winhelp/zeck_lz77.rb +271 -0
- data/lib/cabriolet/huffman/encoder.rb +15 -12
- data/lib/cabriolet/huffman/tree.rb +85 -1
- data/lib/cabriolet/kwaj/command_handler.rb +213 -0
- data/lib/cabriolet/kwaj/compressor.rb +7 -3
- data/lib/cabriolet/kwaj/decompressor.rb +18 -12
- data/lib/cabriolet/lit/command_handler.rb +221 -0
- data/lib/cabriolet/lit/compressor.rb +119 -168
- data/lib/cabriolet/lit/content_encoder.rb +76 -0
- data/lib/cabriolet/lit/content_type_detector.rb +50 -0
- data/lib/cabriolet/lit/decompressor.rb +518 -152
- data/lib/cabriolet/lit/directory_builder.rb +153 -0
- data/lib/cabriolet/lit/guid_generator.rb +16 -0
- data/lib/cabriolet/lit/header_writer.rb +124 -0
- data/lib/cabriolet/lit/parser.rb +670 -0
- data/lib/cabriolet/lit/piece_builder.rb +74 -0
- data/lib/cabriolet/lit/structure_builder.rb +252 -0
- data/lib/cabriolet/models/hlp_file.rb +130 -29
- data/lib/cabriolet/models/hlp_header.rb +105 -17
- data/lib/cabriolet/models/lit_header.rb +212 -25
- data/lib/cabriolet/models/szdd_header.rb +10 -2
- data/lib/cabriolet/models/winhelp_header.rb +127 -0
- data/lib/cabriolet/oab/command_handler.rb +257 -0
- data/lib/cabriolet/oab/compressor.rb +17 -8
- data/lib/cabriolet/oab/decompressor.rb +41 -10
- data/lib/cabriolet/offset_calculator.rb +81 -0
- data/lib/cabriolet/plugin.rb +233 -0
- data/lib/cabriolet/plugin_manager.rb +453 -0
- data/lib/cabriolet/plugin_validator.rb +422 -0
- data/lib/cabriolet/quantum_shared.rb +105 -0
- data/lib/cabriolet/system/io_system.rb +3 -0
- data/lib/cabriolet/system/memory_handle.rb +17 -4
- data/lib/cabriolet/szdd/command_handler.rb +217 -0
- data/lib/cabriolet/szdd/compressor.rb +15 -11
- data/lib/cabriolet/szdd/decompressor.rb +18 -9
- data/lib/cabriolet/version.rb +1 -1
- data/lib/cabriolet.rb +181 -20
- metadata +69 -4
- data/lib/cabriolet/auto.rb +0 -173
- data/lib/cabriolet/parallel.rb +0 -333
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fractor"
|
|
4
|
+
|
|
5
|
+
module Cabriolet
|
|
6
|
+
module Extraction
|
|
7
|
+
# Work item for file extraction using Fractor
|
|
8
|
+
class FileExtractionWork < Fractor::Work
|
|
9
|
+
# Initialize work item for extracting a single file
|
|
10
|
+
#
|
|
11
|
+
# @param file [Object] File object from archive (responds to :name, :data)
|
|
12
|
+
# @param output_dir [String] Output directory path
|
|
13
|
+
# @param preserve_paths [Boolean] Whether to preserve directory structure
|
|
14
|
+
# @param overwrite [Boolean] Whether to overwrite existing files
|
|
15
|
+
def initialize(file, output_dir:, preserve_paths: true, overwrite: false)
|
|
16
|
+
super({
|
|
17
|
+
file: file,
|
|
18
|
+
output_dir: output_dir,
|
|
19
|
+
preserve_paths: preserve_paths,
|
|
20
|
+
overwrite: overwrite,
|
|
21
|
+
})
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# The file object to extract
|
|
25
|
+
#
|
|
26
|
+
# @return [Object] File from archive
|
|
27
|
+
def file
|
|
28
|
+
input[:file]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Output directory for extraction
|
|
32
|
+
#
|
|
33
|
+
# @return [String] Directory path
|
|
34
|
+
def output_dir
|
|
35
|
+
input[:output_dir]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Whether to preserve directory structure
|
|
39
|
+
#
|
|
40
|
+
# @return [Boolean]
|
|
41
|
+
def preserve_paths
|
|
42
|
+
input[:preserve_paths]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Whether to overwrite existing files
|
|
46
|
+
#
|
|
47
|
+
# @return [Boolean]
|
|
48
|
+
def overwrite
|
|
49
|
+
input[:overwrite]
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Unique identifier for this work item (filename based)
|
|
53
|
+
#
|
|
54
|
+
# @return [String] Unique identifier
|
|
55
|
+
def id
|
|
56
|
+
file.name
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module Cabriolet
|
|
6
|
+
module Extraction
|
|
7
|
+
# Worker for extracting files using Fractor
|
|
8
|
+
class FileExtractionWorker < Fractor::Worker
|
|
9
|
+
# Process a file extraction work item
|
|
10
|
+
#
|
|
11
|
+
# @param work [FileExtractionWork] Work item to process
|
|
12
|
+
# @return [Fractor::WorkResult] Result of extraction
|
|
13
|
+
def process(work)
|
|
14
|
+
output_path = build_output_path(work)
|
|
15
|
+
|
|
16
|
+
# Check if file exists and skip if not overwriting
|
|
17
|
+
if ::File.exist?(output_path) && !work.overwrite
|
|
18
|
+
return skipped_result(work, "File already exists")
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Create parent directory
|
|
22
|
+
dir = ::File.dirname(output_path)
|
|
23
|
+
FileUtils.mkdir_p(dir) unless ::File.directory?(dir)
|
|
24
|
+
|
|
25
|
+
# Get file data
|
|
26
|
+
data = work.file.data
|
|
27
|
+
unless data
|
|
28
|
+
return skipped_result(work, "No data available")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Write file data
|
|
32
|
+
::File.binwrite(output_path, data)
|
|
33
|
+
|
|
34
|
+
# Preserve file attributes if available
|
|
35
|
+
preserve_file_attributes(output_path, work.file)
|
|
36
|
+
|
|
37
|
+
# Return success result
|
|
38
|
+
Fractor::WorkResult.new(
|
|
39
|
+
result: {
|
|
40
|
+
path: output_path,
|
|
41
|
+
size: data.bytesize,
|
|
42
|
+
name: work.file.name,
|
|
43
|
+
},
|
|
44
|
+
work: work,
|
|
45
|
+
)
|
|
46
|
+
rescue StandardError => e
|
|
47
|
+
# Return error result
|
|
48
|
+
Fractor::WorkResult.new(
|
|
49
|
+
error: {
|
|
50
|
+
message: e.message,
|
|
51
|
+
class: e.class.name,
|
|
52
|
+
backtrace: e.backtrace.first(5),
|
|
53
|
+
},
|
|
54
|
+
work: work,
|
|
55
|
+
)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
# Build the output path for a file
|
|
61
|
+
#
|
|
62
|
+
# @param work [FileExtractionWork] Work item containing file and options
|
|
63
|
+
# @return [String] Full output path
|
|
64
|
+
def build_output_path(work)
|
|
65
|
+
# Normalize path separators (Windows archives use backslashes)
|
|
66
|
+
clean_name = work.file.name.gsub("\\", "/")
|
|
67
|
+
|
|
68
|
+
if work.preserve_paths
|
|
69
|
+
::File.join(work.output_dir, clean_name)
|
|
70
|
+
else
|
|
71
|
+
::File.join(work.output_dir, ::File.basename(clean_name))
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Preserve file attributes (timestamps, etc.)
|
|
76
|
+
#
|
|
77
|
+
# @param path [String] Path to extracted file
|
|
78
|
+
# @param file [Object] File object from archive
|
|
79
|
+
def preserve_file_attributes(path, file)
|
|
80
|
+
# Try various timestamp attributes that different formats use
|
|
81
|
+
if file.respond_to?(:datetime) && file.datetime
|
|
82
|
+
::File.utime(::File.atime(path), file.datetime, path)
|
|
83
|
+
elsif file.respond_to?(:mtime) && file.mtime
|
|
84
|
+
atime = file.respond_to?(:atime) ? file.atime : ::File.atime(path)
|
|
85
|
+
::File.utime(atime, file.mtime, path)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Create a skipped result
|
|
90
|
+
#
|
|
91
|
+
# @param work [FileExtractionWork] Work item that was skipped
|
|
92
|
+
# @param reason [String] Reason for skipping
|
|
93
|
+
# @return [Fractor::WorkResult] Skipped result
|
|
94
|
+
def skipped_result(work, reason)
|
|
95
|
+
Fractor::WorkResult.new(
|
|
96
|
+
result: {
|
|
97
|
+
status: :skipped,
|
|
98
|
+
name: work.file.name,
|
|
99
|
+
reason: reason,
|
|
100
|
+
},
|
|
101
|
+
work: work,
|
|
102
|
+
)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cabriolet
|
|
4
|
+
# Represents a file to be added to an archive
|
|
5
|
+
#
|
|
6
|
+
# Single responsibility: Encapsulate file metadata and data access.
|
|
7
|
+
# Supports both disk files and memory data, providing unified interface
|
|
8
|
+
# for file operations across all format compressors.
|
|
9
|
+
#
|
|
10
|
+
# @example Adding a disk file
|
|
11
|
+
# entry = FileEntry.new(
|
|
12
|
+
# source: "/path/to/file.txt",
|
|
13
|
+
# archive_path: "docs/file.txt"
|
|
14
|
+
# )
|
|
15
|
+
#
|
|
16
|
+
# @example Adding memory data
|
|
17
|
+
# entry = FileEntry.new(
|
|
18
|
+
# data: "Hello, World!",
|
|
19
|
+
# archive_path: "greeting.txt"
|
|
20
|
+
# )
|
|
21
|
+
class FileEntry
|
|
22
|
+
attr_reader :source_path, :archive_path, :data, :options
|
|
23
|
+
|
|
24
|
+
# Initialize a file entry
|
|
25
|
+
#
|
|
26
|
+
# @param source [String, nil] Path to source file on disk
|
|
27
|
+
# @param data [String, nil] File data in memory
|
|
28
|
+
# @param archive_path [String] Path within the archive
|
|
29
|
+
# @param options [Hash] Format-specific options
|
|
30
|
+
# @raise [ArgumentError] if validation fails
|
|
31
|
+
def initialize(archive_path:, source: nil, data: nil, **options)
|
|
32
|
+
@source_path = source
|
|
33
|
+
@data = data
|
|
34
|
+
@archive_path = archive_path
|
|
35
|
+
@options = options
|
|
36
|
+
|
|
37
|
+
validate!
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Check if file data is from disk
|
|
41
|
+
#
|
|
42
|
+
# @return [Boolean] true if file is on disk
|
|
43
|
+
def from_disk?
|
|
44
|
+
!@source_path.nil?
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Check if file data is in memory
|
|
48
|
+
#
|
|
49
|
+
# @return [Boolean] true if data is in memory
|
|
50
|
+
def from_memory?
|
|
51
|
+
!@data.nil?
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Read file data (from disk or memory)
|
|
55
|
+
#
|
|
56
|
+
# @return [String] File contents
|
|
57
|
+
def read_data
|
|
58
|
+
return @data if from_memory?
|
|
59
|
+
|
|
60
|
+
File.binread(@source_path)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Get file size
|
|
64
|
+
#
|
|
65
|
+
# @return [Integer] File size in bytes
|
|
66
|
+
def size
|
|
67
|
+
return @data.bytesize if from_memory?
|
|
68
|
+
|
|
69
|
+
File.size(@source_path)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Get file stat (disk files only)
|
|
73
|
+
#
|
|
74
|
+
# @return [File::Stat, nil] File stat or nil for memory files
|
|
75
|
+
def stat
|
|
76
|
+
return nil if from_memory?
|
|
77
|
+
|
|
78
|
+
File.stat(@source_path)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Get modification time
|
|
82
|
+
#
|
|
83
|
+
# @return [Time] Modification time (current time for memory files)
|
|
84
|
+
def mtime
|
|
85
|
+
return Time.now if from_memory?
|
|
86
|
+
|
|
87
|
+
stat&.mtime || Time.now
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Get file attributes
|
|
91
|
+
#
|
|
92
|
+
# @return [Integer] File attributes flags
|
|
93
|
+
def attributes
|
|
94
|
+
return @options[:attributes] if @options[:attributes]
|
|
95
|
+
return Constants::ATTRIB_ARCH if from_memory?
|
|
96
|
+
|
|
97
|
+
calculate_disk_attributes
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Get compression flag from options
|
|
101
|
+
#
|
|
102
|
+
# @return [Boolean] Whether to compress this file
|
|
103
|
+
def compress?
|
|
104
|
+
@options.fetch(:compress, true)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
private
|
|
108
|
+
|
|
109
|
+
# Validate entry parameters
|
|
110
|
+
#
|
|
111
|
+
# @raise [ArgumentError] if invalid
|
|
112
|
+
def validate!
|
|
113
|
+
if @source_path.nil? && @data.nil?
|
|
114
|
+
raise ArgumentError,
|
|
115
|
+
"Must provide either source or data"
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
if @source_path && @data
|
|
119
|
+
raise ArgumentError,
|
|
120
|
+
"Cannot provide both source and data"
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
if @source_path
|
|
124
|
+
unless File.exist?(@source_path)
|
|
125
|
+
raise ArgumentError,
|
|
126
|
+
"File not found: #{@source_path}"
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
unless File.file?(@source_path)
|
|
130
|
+
raise ArgumentError,
|
|
131
|
+
"Not a file: #{@source_path}"
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
raise ArgumentError, "Archive path required" if @archive_path.nil?
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Calculate attributes from disk file stat
|
|
139
|
+
#
|
|
140
|
+
# @return [Integer] Attribute flags
|
|
141
|
+
def calculate_disk_attributes
|
|
142
|
+
file_stat = stat
|
|
143
|
+
return Constants::ATTRIB_ARCH unless file_stat
|
|
144
|
+
|
|
145
|
+
attribs = Constants::ATTRIB_ARCH
|
|
146
|
+
|
|
147
|
+
# Read-only flag
|
|
148
|
+
attribs |= Constants::ATTRIB_READONLY unless file_stat.writable?
|
|
149
|
+
|
|
150
|
+
# Executable flag (Unix systems)
|
|
151
|
+
attribs |= Constants::ATTRIB_EXEC if file_stat.executable?
|
|
152
|
+
|
|
153
|
+
attribs
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "file_entry"
|
|
4
|
+
|
|
5
|
+
module Cabriolet
|
|
6
|
+
# Manages collection of files for archive creation
|
|
7
|
+
#
|
|
8
|
+
# Single responsibility: File list management and enumeration.
|
|
9
|
+
# Provides unified interface for adding files from disk or memory,
|
|
10
|
+
# and supports standard Ruby enumeration patterns.
|
|
11
|
+
#
|
|
12
|
+
# @example Basic usage
|
|
13
|
+
# manager = FileManager.new
|
|
14
|
+
# manager.add_file("/path/to/file.txt", "docs/file.txt")
|
|
15
|
+
# manager.add_data("Hello", "greeting.txt")
|
|
16
|
+
# manager.each { |entry| puts entry.archive_path }
|
|
17
|
+
class FileManager
|
|
18
|
+
include Enumerable
|
|
19
|
+
|
|
20
|
+
# Initialize empty file manager
|
|
21
|
+
def initialize
|
|
22
|
+
@entries = []
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Add file from disk
|
|
26
|
+
#
|
|
27
|
+
# @param source_path [String] Path to source file
|
|
28
|
+
# @param archive_path [String, nil] Path in archive (nil = use basename)
|
|
29
|
+
# @param options [Hash] Format-specific options
|
|
30
|
+
# @return [FileEntry] Added entry
|
|
31
|
+
# @raise [ArgumentError] if file doesn't exist
|
|
32
|
+
def add_file(source_path, archive_path = nil, **options)
|
|
33
|
+
archive_path ||= File.basename(source_path)
|
|
34
|
+
|
|
35
|
+
entry = FileEntry.new(
|
|
36
|
+
source: source_path,
|
|
37
|
+
archive_path: archive_path,
|
|
38
|
+
**options,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
@entries << entry
|
|
42
|
+
entry
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Add file from memory
|
|
46
|
+
#
|
|
47
|
+
# @param data [String] File data
|
|
48
|
+
# @param archive_path [String] Path in archive
|
|
49
|
+
# @param options [Hash] Format-specific options
|
|
50
|
+
# @return [FileEntry] Added entry
|
|
51
|
+
def add_data(data, archive_path, **options)
|
|
52
|
+
entry = FileEntry.new(
|
|
53
|
+
data: data,
|
|
54
|
+
archive_path: archive_path,
|
|
55
|
+
**options,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
@entries << entry
|
|
59
|
+
entry
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Enumerate entries (Enumerable interface)
|
|
63
|
+
#
|
|
64
|
+
# @yield [FileEntry] Each file entry
|
|
65
|
+
def each(&)
|
|
66
|
+
@entries.each(&)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Check if empty
|
|
70
|
+
#
|
|
71
|
+
# @return [Boolean] true if no files added
|
|
72
|
+
def empty?
|
|
73
|
+
@entries.empty?
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Get count of entries
|
|
77
|
+
#
|
|
78
|
+
# @return [Integer] Number of entries
|
|
79
|
+
def size
|
|
80
|
+
@entries.size
|
|
81
|
+
end
|
|
82
|
+
alias count size
|
|
83
|
+
|
|
84
|
+
# Get entry by index
|
|
85
|
+
#
|
|
86
|
+
# @param index [Integer] Entry index
|
|
87
|
+
# @return [FileEntry, nil] Entry or nil if out of bounds
|
|
88
|
+
def [](index)
|
|
89
|
+
@entries[index]
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Get all entries
|
|
93
|
+
#
|
|
94
|
+
# @return [Array<FileEntry>] Copy of entries array
|
|
95
|
+
def all
|
|
96
|
+
@entries.dup
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Clear all entries
|
|
100
|
+
#
|
|
101
|
+
# @return [self]
|
|
102
|
+
def clear
|
|
103
|
+
@entries.clear
|
|
104
|
+
self
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Calculate total size of all files
|
|
108
|
+
#
|
|
109
|
+
# @return [Integer] Total size in bytes
|
|
110
|
+
def total_size
|
|
111
|
+
@entries.sum(&:size)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Get files from disk
|
|
115
|
+
#
|
|
116
|
+
# @return [Array<FileEntry>] Disk-based entries
|
|
117
|
+
def disk_files
|
|
118
|
+
@entries.select(&:from_disk?)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Get files from memory
|
|
122
|
+
#
|
|
123
|
+
# @return [Array<FileEntry>] Memory-based entries
|
|
124
|
+
def memory_files
|
|
125
|
+
@entries.select(&:from_memory?)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Find entry by archive path
|
|
129
|
+
#
|
|
130
|
+
# @param path [String] Archive path to find
|
|
131
|
+
# @return [FileEntry, nil] Entry or nil if not found
|
|
132
|
+
def find_by_path(path)
|
|
133
|
+
@entries.find { |entry| entry.archive_path == path }
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Check if archive path exists
|
|
137
|
+
#
|
|
138
|
+
# @param path [String] Archive path to check
|
|
139
|
+
# @return [Boolean] true if path exists
|
|
140
|
+
def path_exists?(path)
|
|
141
|
+
!find_by_path(path).nil?
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cabriolet
|
|
4
|
+
# FormatBase provides common functionality for all format-specific compressors
|
|
5
|
+
# and decompressors, reducing code duplication and establishing consistent patterns.
|
|
6
|
+
class FormatBase
|
|
7
|
+
# Initialize a format handler with common dependencies
|
|
8
|
+
#
|
|
9
|
+
# @param io_system [System::IOSystem, nil] I/O system for file operations
|
|
10
|
+
# @param algorithm_factory [AlgorithmFactory, nil] Factory for compression algorithms
|
|
11
|
+
def initialize(io_system = nil, algorithm_factory = nil)
|
|
12
|
+
@io_system = io_system || System::IOSystem.new
|
|
13
|
+
@algorithm_factory = algorithm_factory || Cabriolet.algorithm_factory
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
protected
|
|
17
|
+
|
|
18
|
+
# Execute a block with file handles, automatically closing them after completion
|
|
19
|
+
#
|
|
20
|
+
# @param input_path [String] Path to input file
|
|
21
|
+
# @param output_path [String, nil] Path to output file (optional)
|
|
22
|
+
# @yield [Array<System::FileHandle>] File handles for input and output
|
|
23
|
+
# @return [Object] Return value of the block
|
|
24
|
+
def with_file_handles(input_path, output_path = nil)
|
|
25
|
+
input_handle = @io_system.open(input_path, Constants::MODE_READ)
|
|
26
|
+
output_handle = if output_path
|
|
27
|
+
@io_system.open(output_path,
|
|
28
|
+
Constants::MODE_WRITE)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
begin
|
|
32
|
+
yield [input_handle, output_handle].compact
|
|
33
|
+
ensure
|
|
34
|
+
@io_system.close(input_handle) if input_handle
|
|
35
|
+
@io_system.close(output_handle) if output_handle
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Create a compressor using the algorithm factory
|
|
40
|
+
#
|
|
41
|
+
# @param algorithm [Symbol] Compression algorithm type
|
|
42
|
+
# @param input [System::FileHandle, System::MemoryHandle] Input handle
|
|
43
|
+
# @param output [System::FileHandle, System::MemoryHandle] Output handle
|
|
44
|
+
# @param size [Integer] Data size
|
|
45
|
+
# @param options [Hash] Additional options for the compressor
|
|
46
|
+
# @return [Object] Compressor instance
|
|
47
|
+
def create_compressor(algorithm, input, output, size, **options)
|
|
48
|
+
@algorithm_factory.create(
|
|
49
|
+
algorithm,
|
|
50
|
+
:compressor,
|
|
51
|
+
@io_system,
|
|
52
|
+
input,
|
|
53
|
+
output,
|
|
54
|
+
size,
|
|
55
|
+
**options,
|
|
56
|
+
)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Create a decompressor using the algorithm factory
|
|
60
|
+
#
|
|
61
|
+
# @param algorithm [Symbol] Compression algorithm type
|
|
62
|
+
# @param input [System::FileHandle, System::MemoryHandle] Input handle
|
|
63
|
+
# @param output [System::FileHandle, System::MemoryHandle] Output handle
|
|
64
|
+
# @param size [Integer] Data size
|
|
65
|
+
# @param options [Hash] Additional options for the decompressor
|
|
66
|
+
# @return [Object] Decompressor instance
|
|
67
|
+
def create_decompressor(algorithm, input, output, size, **options)
|
|
68
|
+
@algorithm_factory.create(
|
|
69
|
+
algorithm,
|
|
70
|
+
:decompressor,
|
|
71
|
+
@io_system,
|
|
72
|
+
input,
|
|
73
|
+
output,
|
|
74
|
+
size,
|
|
75
|
+
**options,
|
|
76
|
+
)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|