cabriolet 0.1.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.
- checksums.yaml +7 -0
- data/ARCHITECTURE.md +799 -0
- data/CHANGELOG.md +44 -0
- data/LICENSE +29 -0
- data/README.adoc +1207 -0
- data/exe/cabriolet +6 -0
- data/lib/cabriolet/auto.rb +173 -0
- data/lib/cabriolet/binary/bitstream.rb +148 -0
- data/lib/cabriolet/binary/bitstream_writer.rb +180 -0
- data/lib/cabriolet/binary/chm_structures.rb +213 -0
- data/lib/cabriolet/binary/hlp_structures.rb +66 -0
- data/lib/cabriolet/binary/kwaj_structures.rb +74 -0
- data/lib/cabriolet/binary/lit_structures.rb +107 -0
- data/lib/cabriolet/binary/oab_structures.rb +112 -0
- data/lib/cabriolet/binary/structures.rb +56 -0
- data/lib/cabriolet/binary/szdd_structures.rb +60 -0
- data/lib/cabriolet/cab/compressor.rb +382 -0
- data/lib/cabriolet/cab/decompressor.rb +510 -0
- data/lib/cabriolet/cab/extractor.rb +357 -0
- data/lib/cabriolet/cab/parser.rb +264 -0
- data/lib/cabriolet/chm/compressor.rb +513 -0
- data/lib/cabriolet/chm/decompressor.rb +436 -0
- data/lib/cabriolet/chm/parser.rb +254 -0
- data/lib/cabriolet/cli.rb +776 -0
- data/lib/cabriolet/compressors/base.rb +34 -0
- data/lib/cabriolet/compressors/lzss.rb +250 -0
- data/lib/cabriolet/compressors/lzx.rb +581 -0
- data/lib/cabriolet/compressors/mszip.rb +315 -0
- data/lib/cabriolet/compressors/quantum.rb +446 -0
- data/lib/cabriolet/constants.rb +75 -0
- data/lib/cabriolet/decompressors/base.rb +39 -0
- data/lib/cabriolet/decompressors/lzss.rb +138 -0
- data/lib/cabriolet/decompressors/lzx.rb +726 -0
- data/lib/cabriolet/decompressors/mszip.rb +390 -0
- data/lib/cabriolet/decompressors/none.rb +27 -0
- data/lib/cabriolet/decompressors/quantum.rb +456 -0
- data/lib/cabriolet/errors.rb +39 -0
- data/lib/cabriolet/format_detector.rb +156 -0
- data/lib/cabriolet/hlp/compressor.rb +272 -0
- data/lib/cabriolet/hlp/decompressor.rb +198 -0
- data/lib/cabriolet/hlp/parser.rb +131 -0
- data/lib/cabriolet/huffman/decoder.rb +79 -0
- data/lib/cabriolet/huffman/encoder.rb +108 -0
- data/lib/cabriolet/huffman/tree.rb +138 -0
- data/lib/cabriolet/kwaj/compressor.rb +479 -0
- data/lib/cabriolet/kwaj/decompressor.rb +237 -0
- data/lib/cabriolet/kwaj/parser.rb +183 -0
- data/lib/cabriolet/lit/compressor.rb +255 -0
- data/lib/cabriolet/lit/decompressor.rb +250 -0
- data/lib/cabriolet/models/cabinet.rb +81 -0
- data/lib/cabriolet/models/chm_file.rb +28 -0
- data/lib/cabriolet/models/chm_header.rb +67 -0
- data/lib/cabriolet/models/chm_section.rb +38 -0
- data/lib/cabriolet/models/file.rb +119 -0
- data/lib/cabriolet/models/folder.rb +102 -0
- data/lib/cabriolet/models/folder_data.rb +21 -0
- data/lib/cabriolet/models/hlp_file.rb +45 -0
- data/lib/cabriolet/models/hlp_header.rb +37 -0
- data/lib/cabriolet/models/kwaj_header.rb +98 -0
- data/lib/cabriolet/models/lit_header.rb +55 -0
- data/lib/cabriolet/models/oab_header.rb +95 -0
- data/lib/cabriolet/models/szdd_header.rb +72 -0
- data/lib/cabriolet/modifier.rb +326 -0
- data/lib/cabriolet/oab/compressor.rb +353 -0
- data/lib/cabriolet/oab/decompressor.rb +315 -0
- data/lib/cabriolet/parallel.rb +333 -0
- data/lib/cabriolet/repairer.rb +288 -0
- data/lib/cabriolet/streaming.rb +221 -0
- data/lib/cabriolet/system/file_handle.rb +107 -0
- data/lib/cabriolet/system/io_system.rb +87 -0
- data/lib/cabriolet/system/memory_handle.rb +105 -0
- data/lib/cabriolet/szdd/compressor.rb +217 -0
- data/lib/cabriolet/szdd/decompressor.rb +184 -0
- data/lib/cabriolet/szdd/parser.rb +127 -0
- data/lib/cabriolet/validator.rb +332 -0
- data/lib/cabriolet/version.rb +5 -0
- data/lib/cabriolet.rb +104 -0
- metadata +157 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cabriolet
|
|
4
|
+
module Models
|
|
5
|
+
# OAB (Outlook Offline Address Book) file header
|
|
6
|
+
#
|
|
7
|
+
# OAB files come in two variants:
|
|
8
|
+
# - Full files (version 3.1)
|
|
9
|
+
# - Incremental patches (version 3.2)
|
|
10
|
+
class OABHeader
|
|
11
|
+
attr_accessor :version_hi, :version_lo, :block_max, :target_size, :source_size, :source_crc, :target_crc,
|
|
12
|
+
:is_patch
|
|
13
|
+
|
|
14
|
+
# Create new OAB header
|
|
15
|
+
#
|
|
16
|
+
# @param version_hi [Integer] High version number
|
|
17
|
+
# @param version_lo [Integer] Low version number (1=full, 2=patch)
|
|
18
|
+
# @param block_max [Integer] Maximum block size
|
|
19
|
+
# @param target_size [Integer] Decompressed output size
|
|
20
|
+
# @param source_size [Integer] Base file size (patches only)
|
|
21
|
+
# @param source_crc [Integer] Base file CRC (patches only)
|
|
22
|
+
# @param target_crc [Integer] Target file CRC (patches only)
|
|
23
|
+
def initialize(version_hi:, version_lo:, block_max:, target_size:,
|
|
24
|
+
source_size: nil, source_crc: nil, target_crc: nil)
|
|
25
|
+
@version_hi = version_hi
|
|
26
|
+
@version_lo = version_lo
|
|
27
|
+
@block_max = block_max
|
|
28
|
+
@target_size = target_size
|
|
29
|
+
@source_size = source_size
|
|
30
|
+
@source_crc = source_crc
|
|
31
|
+
@target_crc = target_crc
|
|
32
|
+
@is_patch = (version_lo == 2)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Check if this is a valid OAB header
|
|
36
|
+
#
|
|
37
|
+
# @return [Boolean]
|
|
38
|
+
def valid?
|
|
39
|
+
version_hi == 3 && [1, 2].include?(version_lo)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Check if this is an incremental patch
|
|
43
|
+
#
|
|
44
|
+
# @return [Boolean]
|
|
45
|
+
def patch?
|
|
46
|
+
is_patch
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Check if this is a full file
|
|
50
|
+
#
|
|
51
|
+
# @return [Boolean]
|
|
52
|
+
def full?
|
|
53
|
+
!is_patch
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# OAB block header for full files
|
|
58
|
+
class OABBlockHeader
|
|
59
|
+
attr_accessor :flags, :compressed_size, :uncompressed_size, :crc
|
|
60
|
+
|
|
61
|
+
def initialize(flags:, compressed_size:, uncompressed_size:, crc:)
|
|
62
|
+
@flags = flags
|
|
63
|
+
@compressed_size = compressed_size
|
|
64
|
+
@uncompressed_size = uncompressed_size
|
|
65
|
+
@crc = crc
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Check if block is compressed
|
|
69
|
+
#
|
|
70
|
+
# @return [Boolean]
|
|
71
|
+
def compressed?
|
|
72
|
+
flags == 1
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Check if block is uncompressed
|
|
76
|
+
#
|
|
77
|
+
# @return [Boolean]
|
|
78
|
+
def uncompressed?
|
|
79
|
+
flags.zero?
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# OAB block header for patch files
|
|
84
|
+
class OABPatchBlockHeader
|
|
85
|
+
attr_accessor :patch_size, :target_size, :source_size, :crc
|
|
86
|
+
|
|
87
|
+
def initialize(patch_size:, target_size:, source_size:, crc:)
|
|
88
|
+
@patch_size = patch_size
|
|
89
|
+
@target_size = target_size
|
|
90
|
+
@source_size = source_size
|
|
91
|
+
@crc = crc
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cabriolet
|
|
4
|
+
module Models
|
|
5
|
+
# Represents an SZDD file header
|
|
6
|
+
#
|
|
7
|
+
# SZDD files are single-file compressed archives using LZSS compression.
|
|
8
|
+
# They were commonly used with MS-DOS COMPRESS.EXE and EXPAND.EXE commands.
|
|
9
|
+
class SZDDHeader
|
|
10
|
+
# SZDD format types
|
|
11
|
+
FORMAT_NORMAL = :normal
|
|
12
|
+
FORMAT_QBASIC = :qbasic
|
|
13
|
+
|
|
14
|
+
# Format of the SZDD file (:normal or :qbasic)
|
|
15
|
+
# @return [Symbol]
|
|
16
|
+
attr_accessor :format
|
|
17
|
+
|
|
18
|
+
# Uncompressed file size in bytes
|
|
19
|
+
# @return [Integer]
|
|
20
|
+
attr_accessor :length
|
|
21
|
+
|
|
22
|
+
# Missing character from the original filename (NORMAL format only)
|
|
23
|
+
# Commonly the last character (e.g., 't' in 'file.txt' -> 'file.tx_')
|
|
24
|
+
# @return [String, nil]
|
|
25
|
+
attr_accessor :missing_char
|
|
26
|
+
|
|
27
|
+
# Original or suggested filename
|
|
28
|
+
# @return [String, nil]
|
|
29
|
+
attr_accessor :filename
|
|
30
|
+
|
|
31
|
+
# Initialize a new SZDD header
|
|
32
|
+
#
|
|
33
|
+
# @param format [Symbol] Format type (:normal or :qbasic)
|
|
34
|
+
# @param length [Integer] Uncompressed size
|
|
35
|
+
# @param missing_char [String, nil] Missing filename character
|
|
36
|
+
# @param filename [String, nil] Original filename
|
|
37
|
+
def initialize(format: FORMAT_NORMAL, length: 0, missing_char: nil,
|
|
38
|
+
filename: nil)
|
|
39
|
+
@format = format
|
|
40
|
+
@length = length
|
|
41
|
+
@missing_char = missing_char
|
|
42
|
+
@filename = filename
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Check if this is a NORMAL format SZDD file
|
|
46
|
+
#
|
|
47
|
+
# @return [Boolean]
|
|
48
|
+
def normal_format?
|
|
49
|
+
@format == FORMAT_NORMAL
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Check if this is a QBASIC format SZDD file
|
|
53
|
+
#
|
|
54
|
+
# @return [Boolean]
|
|
55
|
+
def qbasic_format?
|
|
56
|
+
@format == FORMAT_QBASIC
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Generate suggested output filename from compressed filename
|
|
60
|
+
#
|
|
61
|
+
# @param compressed_filename [String] The compressed filename
|
|
62
|
+
# @return [String] Suggested output filename
|
|
63
|
+
def suggested_filename(compressed_filename)
|
|
64
|
+
return compressed_filename unless normal_format? && @missing_char
|
|
65
|
+
|
|
66
|
+
# Replace trailing underscore with missing character
|
|
67
|
+
# Pattern: ends with .XX_ where XX is any 2+ characters
|
|
68
|
+
compressed_filename.sub(/\.(\w+)_$/, ".\\1#{@missing_char}")
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cabriolet
|
|
4
|
+
# Archive modification functionality (add, update, remove files)
|
|
5
|
+
class Modifier
|
|
6
|
+
def initialize(path)
|
|
7
|
+
@path = path
|
|
8
|
+
@format = FormatDetector.detect(path)
|
|
9
|
+
@modifications = []
|
|
10
|
+
@parser_class = FormatDetector.format_to_parser(@format)
|
|
11
|
+
|
|
12
|
+
raise UnsupportedFormatError, "Unknown format: #{path}" unless @format
|
|
13
|
+
|
|
14
|
+
unless @parser_class
|
|
15
|
+
raise UnsupportedFormatError,
|
|
16
|
+
"No parser for format: #{@format}"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Add a file to the archive
|
|
21
|
+
#
|
|
22
|
+
# @param name [String] File name in archive
|
|
23
|
+
# @param source [String, nil] Source file path (nil for data parameter)
|
|
24
|
+
# @param data [String, nil] File data (if source not provided)
|
|
25
|
+
# @param options [Hash] File metadata options
|
|
26
|
+
# @return [self]
|
|
27
|
+
#
|
|
28
|
+
# @example
|
|
29
|
+
# modifier = Cabriolet::Modifier.new('archive.cab')
|
|
30
|
+
# modifier.add_file('new.txt', source: 'path/to/new.txt')
|
|
31
|
+
# modifier.add_file('data.bin', data: binary_data)
|
|
32
|
+
# modifier.save
|
|
33
|
+
def add_file(name, source: nil, data: nil, **options)
|
|
34
|
+
if source.nil? && data.nil?
|
|
35
|
+
raise ArgumentError,
|
|
36
|
+
"Must provide either source or data"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
file_data = source ? File.read(source, mode: "rb") : data
|
|
40
|
+
|
|
41
|
+
@modifications << {
|
|
42
|
+
action: :add,
|
|
43
|
+
name: name,
|
|
44
|
+
data: file_data,
|
|
45
|
+
options: options,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
self
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Update an existing file in the archive
|
|
52
|
+
#
|
|
53
|
+
# @param name [String] File name to update
|
|
54
|
+
# @param source [String, nil] Source file path
|
|
55
|
+
# @param data [String, nil] New file data
|
|
56
|
+
# @return [self]
|
|
57
|
+
#
|
|
58
|
+
# @example
|
|
59
|
+
# modifier.update_file('config.xml', data: new_xml_data)
|
|
60
|
+
def update_file(name, source: nil, data: nil, **options)
|
|
61
|
+
if source.nil? && data.nil?
|
|
62
|
+
raise ArgumentError,
|
|
63
|
+
"Must provide either source or data"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
file_data = source ? File.read(source, mode: "rb") : data
|
|
67
|
+
|
|
68
|
+
@modifications << {
|
|
69
|
+
action: :update,
|
|
70
|
+
name: name,
|
|
71
|
+
data: file_data,
|
|
72
|
+
options: options,
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
self
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Remove a file from the archive
|
|
79
|
+
#
|
|
80
|
+
# @param name [String] File name to remove
|
|
81
|
+
# @return [self]
|
|
82
|
+
#
|
|
83
|
+
# @example
|
|
84
|
+
# modifier.remove_file('old.txt')
|
|
85
|
+
def remove_file(name)
|
|
86
|
+
@modifications << {
|
|
87
|
+
action: :remove,
|
|
88
|
+
name: name,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
self
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Rename a file in the archive
|
|
95
|
+
#
|
|
96
|
+
# @param old_name [String] Current file name
|
|
97
|
+
# @param new_name [String] New file name
|
|
98
|
+
# @return [self]
|
|
99
|
+
#
|
|
100
|
+
# @example
|
|
101
|
+
# modifier.rename_file('old_name.txt', 'new_name.txt')
|
|
102
|
+
def rename_file(old_name, new_name)
|
|
103
|
+
@modifications << {
|
|
104
|
+
action: :rename,
|
|
105
|
+
old_name: old_name,
|
|
106
|
+
new_name: new_name,
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
self
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Save modifications to the archive
|
|
113
|
+
#
|
|
114
|
+
# @param output [String, nil] Output path (nil for in-place update)
|
|
115
|
+
# @return [ModificationReport] Report of changes made
|
|
116
|
+
#
|
|
117
|
+
# @example
|
|
118
|
+
# modifier.save # Update in-place
|
|
119
|
+
# modifier.save(output: 'modified.cab') # Save to new file
|
|
120
|
+
def save(output: nil)
|
|
121
|
+
output ||= @path
|
|
122
|
+
|
|
123
|
+
# Parse original archive
|
|
124
|
+
archive = @parser_class.new.parse(@path)
|
|
125
|
+
|
|
126
|
+
# Apply modifications
|
|
127
|
+
modified_files = apply_modifications(archive)
|
|
128
|
+
|
|
129
|
+
# Rebuild archive
|
|
130
|
+
rebuild_archive(modified_files, output)
|
|
131
|
+
|
|
132
|
+
ModificationReport.new(
|
|
133
|
+
success: true,
|
|
134
|
+
original: @path,
|
|
135
|
+
output: output,
|
|
136
|
+
modifications: @modifications.count,
|
|
137
|
+
added: @modifications.count { |m| m[:action] == :add },
|
|
138
|
+
updated: @modifications.count { |m| m[:action] == :update },
|
|
139
|
+
removed: @modifications.count { |m| m[:action] == :remove },
|
|
140
|
+
renamed: @modifications.count { |m| m[:action] == :rename },
|
|
141
|
+
)
|
|
142
|
+
rescue StandardError => e
|
|
143
|
+
ModificationReport.new(
|
|
144
|
+
success: false,
|
|
145
|
+
original: @path,
|
|
146
|
+
output: output,
|
|
147
|
+
error: e.message,
|
|
148
|
+
)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Preview modifications without saving
|
|
152
|
+
#
|
|
153
|
+
# @return [Array<Hash>] List of planned modifications
|
|
154
|
+
def preview
|
|
155
|
+
@modifications.map do |mod|
|
|
156
|
+
case mod[:action]
|
|
157
|
+
when :add
|
|
158
|
+
{ action: "ADD", file: mod[:name], size: mod[:data].bytesize }
|
|
159
|
+
when :update
|
|
160
|
+
{ action: "UPDATE", file: mod[:name], size: mod[:data].bytesize }
|
|
161
|
+
when :remove
|
|
162
|
+
{ action: "REMOVE", file: mod[:name] }
|
|
163
|
+
when :rename
|
|
164
|
+
{ action: "RENAME", from: mod[:old_name], to: mod[:new_name] }
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
private
|
|
170
|
+
|
|
171
|
+
def apply_modifications(archive)
|
|
172
|
+
# Start with existing files
|
|
173
|
+
files_map = {}
|
|
174
|
+
archive.files.each do |file|
|
|
175
|
+
files_map[file.name] = file
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Apply each modification
|
|
179
|
+
@modifications.each do |mod|
|
|
180
|
+
case mod[:action]
|
|
181
|
+
when :add
|
|
182
|
+
files_map[mod[:name]] = create_file_object(mod)
|
|
183
|
+
when :update
|
|
184
|
+
if files_map[mod[:name]]
|
|
185
|
+
files_map[mod[:name]] =
|
|
186
|
+
create_file_object(mod)
|
|
187
|
+
end
|
|
188
|
+
when :remove
|
|
189
|
+
files_map.delete(mod[:name])
|
|
190
|
+
when :rename
|
|
191
|
+
if files_map[mod[:old_name]]
|
|
192
|
+
file = files_map.delete(mod[:old_name])
|
|
193
|
+
files_map[mod[:new_name]] = rename_file_object(file, mod[:new_name])
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
files_map.values
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def create_file_object(mod)
|
|
202
|
+
# Create a simple file object with necessary attributes
|
|
203
|
+
FileObject.new(
|
|
204
|
+
name: mod[:name],
|
|
205
|
+
data: mod[:data],
|
|
206
|
+
attributes: mod[:options][:attributes] || 0x20, # Archive attribute
|
|
207
|
+
date: mod[:options][:date],
|
|
208
|
+
time: mod[:options][:time],
|
|
209
|
+
)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def rename_file_object(file, new_name)
|
|
213
|
+
FileObject.new(
|
|
214
|
+
name: new_name,
|
|
215
|
+
data: file.data,
|
|
216
|
+
attributes: file.respond_to?(:attributes) ? file.attributes : 0x20,
|
|
217
|
+
date: file.respond_to?(:date) ? file.date : nil,
|
|
218
|
+
time: file.respond_to?(:time) ? file.time : nil,
|
|
219
|
+
)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def rebuild_archive(files, output)
|
|
223
|
+
case @format
|
|
224
|
+
when :cab
|
|
225
|
+
rebuild_cab(files, output)
|
|
226
|
+
else
|
|
227
|
+
raise UnsupportedOperationError,
|
|
228
|
+
"Modification not supported for #{@format}"
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def rebuild_cab(files, output)
|
|
233
|
+
require_relative "cab/compressor"
|
|
234
|
+
|
|
235
|
+
compressor = CAB::Compressor.new(
|
|
236
|
+
output: output,
|
|
237
|
+
compression: :mszip,
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
files.each do |file|
|
|
241
|
+
compressor.add_file_data(
|
|
242
|
+
file.name,
|
|
243
|
+
file.data,
|
|
244
|
+
attributes: file.attributes,
|
|
245
|
+
date: file.date,
|
|
246
|
+
time: file.time,
|
|
247
|
+
)
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
compressor.compress
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Simple file object for modified files
|
|
254
|
+
class FileObject
|
|
255
|
+
attr_reader :name, :data, :attributes, :date, :time
|
|
256
|
+
|
|
257
|
+
def initialize(name:, data:, attributes: nil, date: nil, time: nil)
|
|
258
|
+
@name = name
|
|
259
|
+
@data = data
|
|
260
|
+
@attributes = attributes
|
|
261
|
+
@date = date
|
|
262
|
+
@time = time
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def size
|
|
266
|
+
@data.bytesize
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# Modification report
|
|
272
|
+
class ModificationReport
|
|
273
|
+
attr_reader :success, :original, :output, :modifications, :added, :updated,
|
|
274
|
+
:removed, :renamed, :error
|
|
275
|
+
|
|
276
|
+
def initialize(success:, original:, output:, modifications: 0, added: 0, updated: 0, removed: 0, renamed: 0,
|
|
277
|
+
error: nil)
|
|
278
|
+
@success = success
|
|
279
|
+
@original = original
|
|
280
|
+
@output = output
|
|
281
|
+
@modifications = modifications
|
|
282
|
+
@added = added
|
|
283
|
+
@updated = updated
|
|
284
|
+
@removed = removed
|
|
285
|
+
@renamed = renamed
|
|
286
|
+
@error = error
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def success?
|
|
290
|
+
@success
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def summary
|
|
294
|
+
if success?
|
|
295
|
+
"Modified #{@modifications} items: +#{@added} ~#{@updated} -#{@removed} →#{@renamed}"
|
|
296
|
+
else
|
|
297
|
+
"Modification failed: #{@error}"
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def detailed_report
|
|
302
|
+
report = ["=" * 60]
|
|
303
|
+
report << "Archive Modification Report"
|
|
304
|
+
report << ("=" * 60)
|
|
305
|
+
report << "Original: #{@original}"
|
|
306
|
+
report << "Output: #{@output}"
|
|
307
|
+
report << "Status: #{success? ? 'SUCCESS' : 'FAILED'}"
|
|
308
|
+
report << ""
|
|
309
|
+
|
|
310
|
+
if success?
|
|
311
|
+
report << "Modifications:"
|
|
312
|
+
report << " Added: #{@added}"
|
|
313
|
+
report << " Updated: #{@updated}"
|
|
314
|
+
report << " Removed: #{@removed}"
|
|
315
|
+
report << " Renamed: #{@renamed}"
|
|
316
|
+
report << " Total: #{@modifications}"
|
|
317
|
+
else
|
|
318
|
+
report << "Error: #{@error}"
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
report << ""
|
|
322
|
+
report << ("=" * 60)
|
|
323
|
+
report.join("\n")
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
end
|